Cron runs your backups, syncs, and cleanup jobs in silence — and it stays just as silent when they break. Here are five real ways to get a page when a cron job fails, or worse, never runs at all. Copy-paste bash, honest tradeoffs, no fluff.
The dangerous thing about cron is not that jobs fail. Jobs fail everywhere. The dangerous thing is that cron was designed in an era when a failed job printed a line to a mailbox someone actually read — and on a modern server, that mailbox goes nowhere. Your nightly backup can exit non-zero for three weeks and the first you hear of it is the morning you need the backup.
So the real question is not "how do I log cron failures" — it is "how do I get alerted the moment a cron job fails." Below are five approaches, from the built-in one almost nobody has configured correctly, up to the only method that also catches the failure cron itself can't see.
Every reliable cron alert has to cover both of these, and most only cover the first:
Cron has alerting built in. Any output a job writes to stdout or stderr gets emailed to the address in the MAILTO variable at the top of your crontab:
MAILTO="you@example.com"
0 3 * * * /usr/local/bin/backup.sh
Why it usually delivers nowhere: this depends on a working local mail transfer agent (MTA) — sendmail, postfix, whatever — that can actually route mail off the box. Most modern cloud servers ship without one, or with one that dumps into a local spool file no human will ever open. Even when it works, mail from a random VM IP with no SPF/DKIM lands in spam. And critically: MAILTO only fires when the job produces output — a silent success and a silent "never ran" look identical. Useful as a last-ditch log, useless as an alert you can trust.
The honest minimum for catching a job that ran and failed is to check its exit code yourself and push a notification on failure. A tiny wrapper does it:
#!/usr/bin/env bash
# backup-wrapped.sh — run the job, alert on failure
set -o pipefail
/usr/local/bin/backup.sh
CODE=$?
if [ "$CODE" -ne 0 ]; then
curl -s -H "Content-Type: application/json" \
-d "{\"content\": \"Backup FAILED (exit $CODE) on $(hostname)\"}" \
"$DISCORD_WEBHOOK_URL"
fi
exit "$CODE"
Point the crontab at the wrapper instead of the job. Now a failed run pushes the exit code straight to Discord, Slack, or any webhook. This is a genuine improvement — but notice it still only covers failure mode one. If cron never fires this wrapper, no message is ever sent, and everything looks fine.
You can point a log shipper or a grep cron at /var/log/syslog (or journalctl -u cron) and alert on error patterns. In practice this is the most brittle option: log formats drift, a job's own errors may never reach syslog, and you are now maintaining a second system to watch the first. It also, again, cannot see the job that never started. Skip it unless you already run centralized logging for other reasons.
Prometheus with the node exporter and the textfile collector can turn a cron job's success timestamp into a metric, and Alertmanager can page when it goes stale. This genuinely covers both failure modes — a job that never runs leaves a stale timestamp, which fires an alert. The catch is weight: you are standing up Prometheus, Alertmanager, exporters, and alerting rules to watch a backup script. For a fleet with existing Prometheus, great. For three servers and a homelab, it is a lot of yak to shave.
Here is the approach that actually matches the shape of the problem. Instead of asking "did the job fail," you flip it around: the job checks in when it succeeds, and the silence is the alarm. This is called a heartbeat, or a dead-man's-switch.
You add one line to the end of your job — a curl to a unique URL — and register that URL with an off-site watcher that expects a ping on a schedule:
0 3 * * * /usr/local/bin/backup.sh && curl -fsS https://pylonmon.com/hb/your-monitor-id
The && is the whole trick: the ping only fires if the backup exits zero. Now think about what the off-site watcher sees:
The job failing (exit non-zero, no ping) and the job never running (server down, cron dead, box off) produce the same signal — silence past the deadline — so a single mechanism catches both failure modes at once.
This is the one method on the list that catches the invisible failure. A job that never starts cannot email you, cannot run a wrapper, cannot write a metric — but it also cannot send its heartbeat, and the absence of that heartbeat is exactly the thing that pages you. Because the watcher lives off-site, it survives the entire server going dark, which an on-box monitor never can.
Be honest about the failure mode you actually fear:
That last one is what PylonMon is built for. You add the curl, set how long silence is allowed (the 26-hour preset suits a nightly job), pick where alerts go — email, Discord, Slack, Telegram, webhook, SMS on paid — and the silence does the rest. It is free for 20 monitors, no card, and it watches from outside your network so it can tell you the box died even when the box can't. If you also want to watch the whole machine (CPU, memory, disk) the same way, the pylon-beacon agent pushes vitals out with zero open ports.
Because cron's built-in email (the MAILTO setting) needs a working mail transfer agent on the server, and most cloud servers don't have one — or the mail is written to a local spool no one reads, or it lands in spam. Cron also only emails when a job produces output, so a job that fails silently, or never runs, sends nothing.
You can't detect that from inside the server — a job that never starts can't send an alert. The standard solution is a dead-man's-switch (heartbeat): the job pings a URL when it succeeds, and an off-site watcher alerts you when the expected ping doesn't arrive by its deadline. The absence of the check-in is the alert.
It's a monitoring pattern where success is signaled by a regular check-in rather than failure by an error. Your job curls a unique URL on success; a watcher expects that ping on a schedule and pages you if it goes missing. It catches both a failed job and a job that never ran, with one mechanism.
Yes. The exit-code wrapper (Method 2) is free and self-hosted. For a hosted heartbeat that also catches jobs that never run, PylonMon's free plan covers 20 monitors with no credit card — enough for a homelab or a small fleet.
Start watching your cron jobs free → or read the heartbeat setup docs.