Linux cron is one of the most dependable schedulers ever written — it starts on boot, runs for years without intervention, and handles millions of jobs across every Linux distribution. The problem isn't cron. The problem is what happens when the jobs cron runs fail silently, and nobody finds out until something breaks downstream.
This guide covers the real approaches to cron job monitoring on Linux: from the basic mail-based default, through log-based approaches, to the heartbeat pattern that catches every failure mode including missed runs.
The default behavior when a cron job produces output (including errors) is to mail that output to the user running the job. On most modern Linux servers, this means the output goes to /var/spool/mail/username — a local mailbox that nobody reads.
There are three reasons this doesn't work in practice:
/var/log, not /var/spool/mail. Unless you've piped the mail somewhere useful, it accumulates unseen./dev/null to stop the mail noise. This silences failures along with the noise.The result: a cron job can stop running entirely — because of a server reboot, a crontab edit, a permissions change, or a dependency failure — and you won't know until a backup is missing, a report hasn't arrived, or a user reports that something stopped working.
The simplest fix is to point the cron mail somewhere you actually read. At the top of your crontab (run crontab -e), add:
MAILTO="ops@yourcompany.com"
Now any job that produces output on stdout or stderr will mail it to that address — provided a working MTA is configured on the host. On Ubuntu/Debian, you can install ssmtp or msmtp as a lightweight relay; on RHEL/CentOS, postfix is usually present.
This catches exit-code failures only if your script writes to stderr on failure. Most scripts don't. And it still doesn't catch missed runs.
Logging job output to a file gives you a searchable audit trail and lets other monitoring tools (like logwatch or fail2ban) consume it:
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
Combine with explicit exit-code checking in the script itself:
#!/bin/bash
set -e # exit on any error
/usr/bin/rsync -a /data/ /backup/data/
if [ $? -eq 0 ]; then
echo "$(date): backup OK"
else
echo "$(date): backup FAILED" >&2
exit 1
fi
Now the log file captures both success and failure. The downside: you still have to read the log file (or set up log monitoring separately) to know something failed. And if the job simply never ran, the log shows nothing — silence, which is indistinguishable from success.
A step up: wrap every cron job in a shell function that captures exit code and emails on failure:
#!/bin/bash
JOB="$1"
shift
"$JOB" "$@"
EXIT_CODE=$?
if [ "$EXIT_CODE" -ne 0 ]; then
echo "Cron job $JOB failed with exit code $EXIT_CODE" \
| mail -s "CRON FAILURE: $JOB" ops@yourcompany.com
fi
Then in your crontab:
0 3 * * * /opt/scripts/cron-wrapper.sh /opt/scripts/backup.sh
This is more reliable for exit-code failures. It still doesn't catch the case where the job never starts — a reboot that clears a temp crontab, a deploy that overwrites /etc/cron.d/, or a cron daemon that stops running.
The only approach that catches every failure mode — including missed runs — is the heartbeat pattern. Your cron job pings an external monitoring service on successful completion. The monitoring service watches for the ping on a schedule. If the ping doesn't arrive, the service alerts you.
This is called a dead man's switch: the absence of a signal is the alert, not the presence of an error.
Adding a heartbeat to any existing cron job takes one line. Append a curl call at the end of the job:
# Your cron job runs, then pings when done:
0 3 * * * /opt/scripts/backup.sh && curl -fsS --retry 3 \
"https://cronwatch.app/ping/YOUR-UNIQUE-ID" > /dev/null 2>&1
The && operator means the ping only fires if the script exits successfully. If the script fails (non-zero exit), the ping doesn't fire. If the server never ran the job at all, the ping doesn't fire. Either way, the monitoring service detects the missing ping and sends an alert.
&& rather than ; ties the ping to job success. With ;, the ping fires even on failure — you'd lose the failure signal. With &&, a failed job looks the same as a missed job to the monitoring service: no ping, send alert.
Good Linux cron monitoring tracks more than just exit codes. Three other signals matter in production:
Configure your monitoring service with the expected schedule and a grace period. A daily job scheduled at 3am should be seen between 3:00 and 3:10. If it isn't seen by 3:10, send an alert. A grace period prevents false alarms from minor timing variations while still catching genuine misses.
A backup that normally takes 4 minutes completing in 3 seconds is almost certainly failing early. Duration tracking catches this. Record the job's start and end time, or use a monitoring service that tracks duration automatically.
On servers with high load or DST transitions, cron jobs can drift from their scheduled window. A job that's supposed to run at midnight may run at 12:05 on a loaded server. If your monitoring uses a tight grace period, this causes false alarms. Use a grace period that accounts for real-world variation — typically 5–10 minutes for most jobs.
Getting the schedule right is the first step to monitoring it. Linux cron uses five fields:
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of week (0 - 7, 0 and 7 are Sunday)
# │ │ │ │ │
* * * * * command
Common schedules:
0 3 * * * # every day at 3am
*/15 * * * * # every 15 minutes
0 9 * * 1-5 # weekdays at 9am
0 0 1 * * # first day of every month at midnight
@reboot # once on startup
If you're unsure what your cron expression actually does, parse it with a visual tool before deploying it to production.
Enter any cron expression and see the next 10 run times in plain English. Verify your schedule before you deploy it.
Parse your cron expression →CronWatch implements the dead man's switch pattern for Linux cron jobs. You add one line to your crontab; CronWatch watches for the heartbeat on the schedule you specify and sends an alert if the ping doesn't arrive.
Setup takes under 5 minutes:
&& curl -fsS "YOUR-PING-URL" to the end of your cron job command.This works for any Linux cron job — system cron (/etc/cron.d/), user crontabs (crontab -e), and Anacron jobs (/etc/cron.daily/). No agents to install, no changes to your server configuration.
CronWatch sends alerts when your Linux cron jobs fail or miss their schedule — using the heartbeat pattern that catches every failure mode, including missed runs.
Join the CronWatch waitlist → Parse a cron expression