How to monitor a cron job (and find out when it silently fails)
Updated 7 August 2026 · by Cassian Wei · 3 min read
The worst thing about a broken cron job is that nothing tells you. The backup that stopped running in March is discovered in June, when you need it. This guide covers why that happens and how to fix it with a dead-man's switch (also called heartbeat or cron monitoring).
Why cron jobs fail silently
- The machine is gone. The VM was resized, migrated or deleted; the container never restarted. Nothing on it can email you that it's dead.
- The schedule never fires. A typo in the crontab, a missing newline at the end of the file, a server timezone change, or
cronitself not running. - The job runs but fails. Disk full, expired credentials, a dependency upgrade. Exit code 1, output to a log nobody reads.
- The job hangs. A stuck lock or network call means it neither succeeds nor fails — it just never finishes.
Log files and MAILTO= only help with the third case, and only if outbound
mail from that box works and someone reads it. The first two cases are invisible from
the inside — which is why monitoring a cron job has to happen from the outside.
The dead-man's-switch pattern
Invert the logic: instead of the job reporting failures, it reports success — a single HTTP request ("ping") at the end of each successful run. An external service expects that ping on schedule. No ping arrives → something is wrong → you get alerted. This catches every failure mode above, including the machine disappearing entirely.
Set it up (2 curl commands)
With Watchpup (free, no card): create an account, then a heartbeat monitor with your job's real schedule:
curl -X POST https://watchpup.watchpup.workers.dev/api/signup \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","password":"at-least-8-chars"}'
# → {"ok":true,"api_key":"wp_..."}
curl -X POST https://watchpup.watchpup.workers.dev/api/monitors \
-H 'authorization: Bearer wp_...' -H 'content-type: application/json' \
-d '{"kind":"heartbeat","name":"nightly-backup","cron":"15 3 * * *","tz":"Europe/Berlin","grace":1800}'
# → returns a ping_url
Then append the ping to the job in your crontab:
15 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 20 https://watchpup.watchpup.workers.dev/ping/<id>
The && matters: the ping only fires when the script exits 0, so a failing
run is treated exactly like a missing one. grace is how long after the scheduled
time a ping may arrive before alerting (here 30 min — set it a bit above your job's normal runtime).
Worth adding
- Explicit failure signal: hit
{ping_url}/failfrom your error handler to alert immediately instead of waiting for the schedule — the POST body becomes the incident detail. - Hung-job detection: hit
{ping_url}/startwhen the run begins. If no success/fail follows within the grace window, you're alerted — and run duration is tracked. Addmax_duration_sto get a warning when a run succeeds but takes suspiciously long. - Ping by email: some systems (RAID controllers, backup appliances,
cron's own MAILTO) can only send email. Every heartbeat monitor also gets a unique ping email address — any delivered message counts as a ping. - Timezones & DST: give the schedule in the job's own zone (
"tz":"Europe/Berlin") so daylight-saving shifts don't cause false alerts. - Reminders: set
renotifyso a still-broken job re-alerts every N minutes until fixed, instead of one message at 3am that scrolls away.
Full details in the heartbeat docs; there's a live example (demo cron job and nightly-db-backup) on the demo status page.
Watchpup is free uptime & cron monitoring — 1-minute checks, heartbeats, TLS/domain expiry, status pages, alerts everywhere. Sign up, try the live demo, or read more guides.