Your nightly database backup job is scheduled for 2:00 AM. Last Tuesday, the server was under load from an unrelated process during that window and the job never triggered. The backup hasn't run in six days. Nobody knows.
This is the missed run problem: cron jobs can silently skip scheduled executions due to server reboots, load spikes, timezone misconfigurations, or crontab drift — and the default behavior is complete silence. This guide covers why jobs miss runs, how to detect a missed run after the fact, and how to get alerted automatically the next time it happens.
This is the most common cause. Standard cron does not have a concept of "missed runs" — if the server is off, rebooting, or the cron daemon isn't running when a job is scheduled, that run simply doesn't happen. There's no backfill, no retry, and no record that anything was skipped.
This matters for jobs that run less frequently than daily. A weekly job scheduled for Sunday at 3 AM will silently skip if the server reboots at 2:58 AM for an unscheduled kernel update. The next run is seven days later.
cron has an optional load average threshold controlled by the -l flag (or CRON_QUEUE_MAX on some systems). When the system load exceeds this threshold, cron defers the job until load drops below the limit. If load stays high for the entire scheduled minute, the job is skipped entirely — no rescheduling, no alert.
Forgetting to end a crontab file with a newline, using tabs where spaces are expected in some cron implementations, or saving a crontab with a syntax error can silently prevent all subsequent jobs in the file from running. Some cron implementations log an error; others don't.
Cron runs in the server's local timezone by default. Daylight saving time changes shift what wall-clock hour cron considers "3:00 AM". A job scheduled for 0 3 * * * may fire at 2 AM or 4 AM (or twice) during DST transitions. If it fires at the wrong time and produces no output, the job still runs — but if the timing expectation was load-sensitive (e.g., "after the nightly batch"), you now have a problem.
Server migrations between AWS regions or cloud providers often silently change the system timezone. A job scheduled for 3 AM EST runs at 3 AM UTC after migration — eight hours earlier than expected.
Jobs that exit immediately due to a missing binary, a script with a syntax error, or an immediate-exit condition leave no logs unless you explicitly redirect output. /path/to/script.sh 2>&1 | logger helps, but if the script exits before logger captures anything useful, the missed run is still invisible in practice.
On Debian/Ubuntu systems:
grep CRON /var/log/syslog | tail -50
On RHEL/CentOS/Amazon Linux:
grep -i cron /var/log/cron | tail -50
Look for entries like CMD (/path/to/your/script.sh). If you see entries for other jobs around your expected run time but nothing for your job, it missed its window. If you see no cron entries at all, the cron daemon may not have been running.
# Check if crond is running
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS
If your cron job writes output to a log file, check whether an entry exists at the expected run time:
# Check if the backup ran at the expected time
grep "$(date -d 'yesterday' '+%Y-%m-%d')" /var/log/nightly-backup.log | tail -5
An absence of output at the expected time is the clearest indicator of a missed run — assuming your job produces consistent timestamped output.
For jobs with durable outputs (database writes, S3 uploads, file modifications), check the output directly:
# When was the backup file last modified?
ls -la /backups/ | tail -5
# When was the last database backup recorded?
psql -c "SELECT MAX(created_at) FROM backups;"
Detecting a missed run after the fact is forensics. What you actually want is an alert within minutes of the scheduled time passing without execution.
The dead man's switch pattern: the job sends a heartbeat to an external service at the end of each run. If the heartbeat doesn't arrive within the expected window, the service alerts you. No heartbeat = missed run alert.
#!/bin/bash
# Your existing job logic
/path/to/your/backup-script.sh
# Heartbeat: send ping after successful completion
curl -s "https://your-monitoring-service.com/heartbeat/YOUR_TOKEN" \
--max-time 5 \
--retry 2 \
-o /dev/null
The monitoring service expects a ping within N minutes of the scheduled run time (configurable grace period). If no ping arrives, it sends an alert to your email. The key advantage: the alert comes from external infrastructure, not from your server — so it fires even when the server is completely down.
MAILTO=you@yourdomain.com
# Cron will email you if this job produces any stdout/stderr
0 2 * * * /path/to/backup.sh 2>&1
This helps with jobs that fail noisily (producing error output). It does not help with jobs that simply don't run — a missed run produces no output, so MAILTO sends nothing.
#!/bin/bash
SCRIPT="/path/to/backup.sh"
LOG="/var/log/backup.log"
EMAIL="you@yourdomain.com"
START=$(date +%s)
timeout 3600 "$SCRIPT" >> "$LOG" 2>&1
EXIT=$?
END=$(date +%s)
DURATION=$((END - START))
if [ $EXIT -ne 0 ]; then
echo "Backup failed (exit $EXIT) after ${DURATION}s" | \
mail -s "ALERT: nightly backup failed" "$EMAIL"
fi
This catches failures and timeouts but still doesn't catch a job that was never scheduled to begin with (server was down, cron daemon stopped).
The only reliable way to catch a missed run (as opposed to a failed run) is external monitoring that expects your job to check in and alerts when it doesn't. This is what services like CronWatch are designed for.
CronWatch uses the dead man's switch pattern: add one curl line to your cron job, and CronWatch sends an alert within minutes if it misses a heartbeat. Works for any scheduled job — cron, Heroku Scheduler, GitHub Actions, cloud functions.
If your job runs, CronWatch stays quiet. If it misses, you get an email.
Join the CronWatch waitlist → Parse your cron expression free| Cause | Detection | Fix |
|---|---|---|
| Server was down | No cron log entries around run time | Dead man's switch heartbeat monitoring |
| cron daemon stopped | systemctl status cron shows inactive | Enable cron on startup + external monitoring |
| High load average | Check /var/log/syslog for load-skip messages | Lower load threshold or use nice / ionice |
| Timezone mismatch | Job runs at wrong time; check TZ env | Use CRON_TZ=UTC directive in crontab |
| Syntax error in crontab | Validate with cron parser | Fix syntax; crontab -e validates on save |
| Script exits instantly | Check exit code; add set -e and logging | Add 2>&1 | logger and exit code checks |
For any of the first three causes, the only reliable detection method is external monitoring that expects a heartbeat and alerts on silence — internal alerting can't fire when the server or cron daemon is down.
Check your cron expression timing with the free cron expression parser to verify your job is scheduled for when you think it is.