Most cron job monitoring focuses on errors: check the exit code, capture stderr, email the output. This catches a meaningful class of failures — but it misses the most dangerous one. A cron job that stops running entirely produces no errors, no output, and no alerts. It just disappears. The dead man's switch pattern is designed specifically to catch this.
The term comes from safety engineering. A dead man's switch is a control mechanism that is triggered — or held inactive — by the active action of a human operator. If the operator becomes incapacitated, the switch activates. In railways and trains, a driver who stops holding the dead man's switch causes the brakes to engage automatically.
Applied to cron job monitoring, the concept inverts the usual alerting model:
The monitoring service doesn't watch for errors. It watches for the expected heartbeat. The absence of the heartbeat is the signal. This catches every failure mode: exit-code failures, jobs that never started, servers that went down, crontabs that were accidentally cleared, and daemons that silently stopped.
Cron is different from long-running services in a way that makes traditional monitoring inadequate.
For a long-running process (a web server, a worker daemon), uptime monitoring works well: the process either responds or it doesn't. If it crashes, you get an alert. You can poll it continuously.
Cron jobs are different. They run for a few seconds or minutes and then exit. There's nothing to poll between runs. The only evidence a job ran is the output it produced — which usually goes nowhere — or an explicit record it wrote. If you miss a single run, the system looks identical from the outside to a system where the job ran successfully. Silence is not evidence of success.
The three cron failure modes a dead man's switch catches that standard error monitoring misses:
Server rebooted without the cron daemon restarting. A deploy clobbered /etc/cron.d/. An @reboot job didn't fire because the server was already running when it was added. A user crontab was edited on the wrong server. None of these produce any error — the job simply doesn't exist anymore from cron's perspective.
This is rare but real. crond gets killed during a resource crunch. A package update restarts it but the restart fails. A container's init process doesn't manage cron correctly. Again: no errors, no output, just silence.
A backup script that "runs successfully" but exits early because the target directory doesn't exist, producing an empty backup. A sync job that exits 0 but transferred 0 bytes. A job that checks for new records and silently finds none — but "none" is wrong because the data pipeline upstream broke. These jobs succeed technically but fail functionally. A heartbeat that includes a meaningful check (row count, file size, checksum) catches this where a simple exit-code check doesn't.
Adding a dead man's switch to any cron job takes one line. After your job command, append a curl call to your monitoring service's ping URL:
# Ping only on success (use && not ;)
0 3 * * * /opt/scripts/backup.sh && \
curl -fsS --retry 3 --max-time 10 \
"https://cronwatch.app/ping/YOUR-UNIQUE-ID" > /dev/null 2>&1
The && operator is important: it means the curl only runs if the script exits with code 0. A failing script produces no ping — the monitoring service sees a missed heartbeat and alerts you. The -fsS flags tell curl to fail silently on HTTP errors while still failing on network errors, and --retry 3 handles transient network blips.
For jobs where you want to ping on both success and failure (so you can distinguish "job ran and failed" from "job never ran"), send a start ping too:
0 3 * * * curl -fsS "https://cronwatch.app/ping/YOUR-ID/start" && \
/opt/scripts/backup.sh && \
curl -fsS "https://cronwatch.app/ping/YOUR-ID"
This gives you three states: job started but no finish ping (hung or crashed mid-run), finish ping without start ping (scheduling issue), and both pings (success).
The grace period is the window after the expected run time during which a late ping is still considered on-time. You need this because:
A reasonable default grace period for most cron jobs is 10–15 minutes. For jobs where a 10-minute delay is itself a problem (e.g. a job that's supposed to process payments every 5 minutes), use a shorter grace period and accept a higher false-alarm rate, or investigate why the job is running late.
For critical jobs — database backups, billing runs, data syncs — consider adding contextual data to the heartbeat ping. Some monitoring services accept a payload that gets stored with the ping record:
#!/bin/bash
set -e
# Run the backup
/usr/bin/pg_dump mydb | gzip > /backup/mydb-$(date +%Y%m%d).sql.gz
# Get the backup size
BACKUP_SIZE=$(stat -c%s "/backup/mydb-$(date +%Y%m%d).sql.gz")
# Ping with size as a sanity check
curl -fsS --max-time 10 \
"https://cronwatch.app/ping/YOUR-ID?bytes=$BACKUP_SIZE" > /dev/null 2>&1
This makes your monitoring logs useful for diagnosis: you can see at a glance whether the backup size is growing, stable, or suspiciously small.
CronWatch implements the dead man's switch pattern for cron jobs. You enter your job's schedule; CronWatch generates a unique ping URL and watches for heartbeats on that schedule. If a heartbeat is late or missing, CronWatch sends an email alert.
If you're not sure what your cron expression actually means — when it runs, how often, in what timezone — parse it first:
Enter any cron expression and see the next 10 run times in plain English. Free, no signup.
Parse your cron expression →Once you know your schedule is correct, set up the dead man's switch:
CronWatch watches for heartbeats from your scheduled jobs and alerts you the moment a job stops running — whether it failed, missed its schedule, or the server went down.
Join the CronWatch waitlist → Parse a cron expression