Cron Job Dead Man's Switch: Heartbeat Monitoring Explained
Updated
Contents
What is a dead man's switch for cron jobs? A dead man's switch is a monitor that alerts you when an expected signal stops arriving, rather than when an error occurs. For cron jobs, the job sends a "heartbeat" ping each time it completes; the monitoring service expects that ping on a schedule, and if it doesn't arrive within a grace period, it raises an alert. This catches the failure mode ordinary monitoring misses — a job that silently stops running altogether, producing no error and no log to inspect.
The term dead man's switch comes from machinery: a control that must be held down to keep a machine running, so that if the operator collapses, the machine stops safely. Software monitoring borrows the idea and flips it — a signal that must keep arriving, so that when it stops, someone gets alerted.
Why Cron Specifically Needs One#
Almost all monitoring is error-driven: something goes wrong, an error is raised, you react to it. That works right up until the failure mode where nothing runs at all. A cron job that's skipped produces no error, no stack trace, no log line — there's simply an absence where an execution should have been. The classic example: a nightly database backup that has run flawlessly for eight months quietly stops, because a deploy replaced the crontab, or the server rebooted and cron didn't restart, or the disk filled and the job couldn't launch. You find out weeks later, at the exact moment you need that backup and it isn't there.
Logs and exit codes, the checks compared in how to know if a cron job ran, only produce evidence when the job runs. Error-driven monitoring is structurally blind to a job that doesn't. A dead man's switch is built for it. How loud each switch should be when it trips depends on the job; the CronRadar Scheduled Job Methodology ranks jobs P1 to P3 and sets severity and routing for each class.
How Heartbeat Monitoring Works#
Three moving parts:
- The ping. Your job makes a tiny HTTP request when it finishes successfully — a single
curlor one SDK call. That request is the heartbeat. - The expected schedule. You tell the monitor when to expect the ping — for example "every day at 02:30" as a cron expression. Now the monitor knows what "on time" means. Some services expect a fixed interval instead; Dead Man's Snitch vs CronRadar compares the two models, and moving heartbeat checks off Opsgenie shows how to convert a rolling interval into a schedule and grace period.
- The grace period. A tolerance window before a late job is flagged, so normal variance (a backup that sometimes takes ten minutes, sometimes twenty) doesn't page you. Configure it in seconds —
gracePeriod=900is a 15-minute window.
If the ping lands inside the window, the monitor is healthy. Late but still in grace: warning. Past grace with no ping: critical, and you're alerted. Because the alert is driven by the absence of the expected signal, it fires even when the job's machine is completely dead — the monitoring lives outside your infrastructure, so it doesn't depend on the thing it's watching. Self-hosted cron monitoring compares running the monitor yourself and when hosted is better.
Success Ping vs. Full Lifecycle#
The minimal pattern is a single completion ping — enough to answer "did it run on schedule?" A richer pattern sends a start ping and then a complete (or fail) ping. That does two extra things: it measures how long each run takes, and it catches a job that started but hung — one that would look fine to a completion-only check because the start happened. Begin with the completion ping; add start/complete once you care about duration or hangs.
Adding It#
The smallest version is one request appended to the crontab line. Keep the application's API key in a curl config file, one line: user = "ck_app_12345_abcdef:", and point curl at it:
30 2 * * * /opt/backup.sh && curl -fsS -m 5 -o /dev/null -K /etc/cronradar/curl.conf https://cron.life/api/v1/ping/backupThe key stays out of the crontab line and the crontab itself. cronie and Debian's cron copy every crontab variable into an X-Cron-Env header of the mail they send with a job's output (cron "No MTA installed" cites the source), and BusyBox crond, the cron in Alpine and many minimal images, recognizes only MAILTO=, SHELL=, and PATH= lines and silently skips any other assignment (crond.c). -K reads curl options from a config file, so the key never appears in the crontab or on the command line; make the file readable only by the job's account. Cron environment variables shows other ways to load a value from a file.
backup is the monitor key. && sends the heartbeat only when the backup exits 0, and -m 5 gives up on the request after five seconds. This line reports to a monitor that already exists: a report creates a missing monitor only when it carries the schedule, and cron turns an unescaped % in a command into a newline (crontab(5)), which makes a URL-encoded schedule awkward to put here.
A wrapper script avoids that and records the full lifecycle. It reports the start, runs the job, captures its exit code, reports complete or fail, and exits with the job's own code. Each report gives up after five seconds and ignores errors, so a monitoring outage can't fail the backup or hide its result:
#!/bin/sh
# /opt/backup-monitored.sh
# The key lives in a file only the job's account can read: cronie and Debian's cron
# copy crontab variables into the headers of the mail they send.
CRONRADAR_API_KEY=$(cat /etc/cronradar/api-key 2>/dev/null)
url="https://cron.life/api/v1/ping/backup"
run_id="backup-$(date +%s)-$$"
report() {
event=$1
shift
curl --fail --silent --show-error --max-time 5 --output /dev/null \
--request POST --get --user "$CRONRADAR_API_KEY:" \
--data-urlencode "schedule=30 2 * * *" \
--data-urlencode "gracePeriod=900" \
--data-urlencode "run_id=$run_id" \
"$@" "$url/$event" || true
}
report start
/opt/backup.sh
rc=$?
if [ "$rc" -eq 0 ]; then
report complete
else
report fail --data-urlencode "message=backup.sh exited with code $rc"
fi
exit "$rc"Make it executable and point the crontab entry at it: 30 2 * * * /opt/backup-monitored.sh. Because every report carries the schedule, the first run creates the monitor, and if the monitor is ever deleted, the next run creates it again. A fail report alerts immediately instead of waiting for the grace period, and a start with no matching finish is flagged as stuck.
CronRadar reads the schedule in UTC. If the server's clock isn't on UTC, send the UTC equivalent of the crontab time. Schedule syntax covers that and one more difference from Linux cron: when both day fields are restricted, CronRadar expects a run only on dates that match both.
Auto-Discovery for Frameworks#
Hand-written reports are one more thing to remember for every new job. If your jobs are Hangfire recurring jobs or Quartz.NET jobs, the CronRadar extensions for Hangfire and Quartz.NET discover them when the scheduler starts and monitor each one, with no per-job wiring. Every discovered job is monitored unless you opt it out. Read each guide's release notice first, because the published packages trail the documented versions.
Other schedulers, including Laravel's scheduler, Celery beat, and node-cron, report over HTTP as shown above. CronRadar SDK setup for them is currently unavailable, and HTTP reporting doesn't discover jobs, so each job needs its own report.
When an alert does fire, why a cron job isn't running walks through the usual causes.
Frequently Asked Questions#
Dead man's switch vs. uptime monitoring — what's the difference?#
Uptime monitoring checks that a URL or server is reachable. A dead man's switch checks that a scheduled job actually ran. They point in opposite directions: uptime pings in, a dead man's switch waits for a ping out. Some uptime tools also have push monitors that wait for a job's ping; Uptime Kuma cron job monitoring covers one.
What if a job legitimately takes longer than usual?#
The grace period absorbs normal variance — set it wide enough to cover your job's worst realistic run time, and you won't get false alarms.
Do I have to register every job manually?#
Not always. The Hangfire and Quartz.NET extensions register recurring jobs when the scheduler starts. For any other job, a report that includes the schedule creates the monitor, so the first real run registers it.
Does it still alert if my whole server goes down?#
Yes — that's the point. The monitor runs externally, so an absent ping from a dead server is exactly what trips the alert.
Can a heartbeat break my cron job?#
Not if the report is written defensively. Cap each request at five seconds, ignore its result, and exit with the job's own status, as the wrapper above does. If the monitoring service is unreachable, the job still runs and keeps its exit code.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.