← All guides

Cron Job Monitoring: Catch Missed, Failed and Stuck Jobs

Updated:

Cron has no idea whether your job worked. It starts the command at the scheduled time and forgets about it - no retry, no alert, and the only error report is an email to a local mailbox nobody reads. That is why backups that "run every night" turn out to have stopped three months ago.

Cron job monitoring closes that gap by watching four failure modes:

Failure Example How to detect it
Missed - the job did not start crontab overwritten in a deploy, cron daemon stopped, server off at the scheduled time Heartbeat: expect a signal every run, alert when it is late
Failed - it started and exited with an error database dump fails on a full disk Exit code != 0
Stuck - it never finishes hung network mount, lock never released Duration limit / timeout
Wrong result - it "succeeded" but did nothing useful backup file of 0 bytes, report with no rows Check the output or the artefact

1. Detect Missed Runs With a Heartbeat

A heartbeat (also called a dead man's switch) inverts the logic: the job reports in when it finishes, and the monitor alerts when a report does not arrive within the expected window. It is the only reliable way to notice a job that never started.

# m h dom mon dow  command
0 2 * * *  /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 https://hc.example.com/ping/<uuid> > /dev/null

The && matters: the ping is only sent when the job succeeds, so both "missed" and "failed" end in a missing heartbeat. Set the grace period to the job's normal duration plus a margin.

Heartbeat services include Healthchecks.io (free for 20 checks, self-hostable), Better Stack heartbeats and Cronitor. More on the pattern in cron heartbeat monitoring.

2. Catch Failures With the Exit Code

Wrap jobs in a small runner that records the exit code and the output, so a failure leaves evidence:

#!/bin/sh
name="$1"; shift
log="/var/log/cron-jobs/$name.log"
mkdir -p /var/log/cron-jobs
start=$(date +%s)
"$@" > "$log.tmp" 2>&1
code=$?
end=$(date +%s)
{ echo "== $(date -Is) exit=$code duration=$((end-start))s"; cat "$log.tmp"; } >> "$log"
rm -f "$log.tmp"
[ "$code" -eq 0 ] || logger -t cron-job "$name failed with exit $code"
exit "$code"
0 2 * * *  /usr/local/bin/cronwrap backup /usr/local/bin/backup.sh

Failures now appear in syslog (journalctl -t cron-job) and in a per-job log with timestamps and durations.

3. Stop Stuck and Overlapping Runs

A job that hangs blocks the next run or, worse, runs in parallel with it. Add a timeout and a lock:

*/15 * * * *  flock -n /run/lock/sync.lock timeout 10m /usr/local/bin/sync.sh

flock -n skips the run if the previous one still holds the lock; timeout 10m kills it after 10 minutes and returns exit code 124, which your failure check then reports.

4. Verify the Result, Not Just the Run

Exit code 0 only means the script did not report an error. For jobs that produce something, check the artefact:

# newest backup must be younger than 26 hours and larger than 1 MB
f=$(ls -t /backups/*.sql.gz 2>/dev/null | head -1)
[ -n "$f" ] && [ $(( $(date +%s) - $(stat -c %Y "$f") )) -lt 93600 ] && [ $(stat -c %s "$f") -gt 1048576 ] \
  || echo "backup missing, old or too small: ${f:-none}"

This is the check that would have caught most "silent" backup failures: it does not care why the backup is missing, only that it is.

Find Out Why a Job Did Not Run

systemctl status cron                         # is the daemon running? (crond on RHEL)
grep CRON /var/log/syslog | tail -20          # Debian/Ubuntu
journalctl -u cron --since today              # systemd hosts
journalctl -u crond --since today             # RHEL, Rocky, Alma
crontab -l -u deploy                          # is the entry still there?

The usual causes - a different PATH in cron, % not escaped, missing newline at the end of the crontab, wrong user - are covered in cron jobs not executing. To see what is scheduled at all, start with how to list all cron jobs, and for past runs check cron job execution history.

systemd Timers

Timers log every run to the journal and know the exit status, which makes monitoring easier:

systemctl list-timers --all                       # next and last run of every timer
systemctl status backup.service                   # result of the last run
journalctl -u backup.service --since "2 days ago"
systemctl --failed                                # any failed timer service shows up here

Add > to the service unit to run a notification unit whenever the job fails.

What to Monitor First

Not every cron job needs an alert. Start with jobs whose silent failure costs money or data:

  1. Backups and backup uploads
  2. Certificate renewal (certbot renew)
  3. Billing, invoicing and payment syncs
  4. Data imports and exports other systems depend on
  5. Cleanup jobs that keep disks from filling up (log rotation, temp files)

Monitoring Cron Jobs With Zuzia

Zuzia takes a different approach from a heartbeat service: instead of waiting for your crontab to ping it, it runs the scheduled task on the server itself. You set the command, the schedule and the host in the panel; Zuzia executes it and keeps the output of every run with an AI summary, so you can read what a backup or cleanup actually did last night without SSH. The verification commands above (backup age and size, systemctl --failed) make good scheduled tasks too. Comparison with a pure heartbeat tool: Zuzia vs Healthchecks.

FAQ

How do I monitor cron jobs?

Combine a heartbeat for missed runs, the exit code for failures, a timeout for stuck runs and a check of the result (for example the age and size of the backup file). A heartbeat alone covers missed and failed runs if the ping is sent only on success.

How do I know if a cron job ran?

Check the cron log (grep CRON /var/log/syslog or journalctl -u cron) for the start, and your job's own log or heartbeat for the finish. Cron itself only logs that it started the command.

Why did my cron job not run?

Most often: the cron daemon is stopped, the crontab entry was removed or overwritten, the command fails because cron has a minimal PATH, or the server was off at the scheduled time. See cron jobs not executing.

What is a cron heartbeat?

A request the job sends to a monitoring service when it finishes. The service knows the schedule and alerts when the request does not arrive on time.

Should every cron job be monitored?

No. Monitor jobs whose failure loses data, money or availability - backups, renewals, billing, syncs and disk cleanup. Alerting on every minor job leads to ignored alerts.

How do I get an email when a cron job fails?

Set [email protected] at the top of the crontab and make sure the server can send mail; cron mails any output. Better: send only on failure (|| mail ...) or use a heartbeat service, because cron mail is easy to lose and noisy.

We use cookies to ensure the proper functioning of our website.