← Blog  ·  October 2026

Cron Job Missed Run: How to Get Alerted When a Scheduled Job Doesn't Fire

A cron job that silently misses its scheduled run is often worse than one that fails noisily — at least a failure generates a log entry. A missed run generates nothing.

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.

⏰ Check your cron expressions: Free cron expression parser — see your next 5 scheduled run times →

Why cron jobs miss scheduled runs

Server was down during the scheduled window

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.

Load average too high — cron load average limit

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.

crontab edited incorrectly

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.

Timezone mismatch after DST or server migration

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.

The cron job itself exits in under a second

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.

Key insight: Cron's default behavior on a missed or failed run is silence. You must explicitly instrument your cron jobs to make failures visible.

How to detect a cron job missed run after the fact

Check the system cron log

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

Check application logs for expected output

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.

Check the job's side effects

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;"

How to get alerted on the next missed run

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.

Method 1: Add a heartbeat ping to the job

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.

Method 2: Use cron's MAILTO directive for immediate failure output

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.

Method 3: Wrap the job with a timeout + alert

#!/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).

Method 4: Dedicated cron monitoring service

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.

⏰ Stop flying blind on missed cron runs

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

Quick reference: missed run causes and fixes

CauseDetectionFix
Server was downNo cron log entries around run timeDead man's switch heartbeat monitoring
cron daemon stoppedsystemctl status cron shows inactiveEnable cron on startup + external monitoring
High load averageCheck /var/log/syslog for load-skip messagesLower load threshold or use nice / ionice
Timezone mismatchJob runs at wrong time; check TZ envUse CRON_TZ=UTC directive in crontab
Syntax error in crontabValidate with cron parserFix syntax; crontab -e validates on save
Script exits instantlyCheck exit code; add set -e and loggingAdd 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.