A cron job with no alert is a job you are not running
The claim A scheduled job that runs silently and is only noticed when it fails is not a reliable job — it is a job that has already failed silently at least once and nobody knew. C...
The claim
A scheduled job that runs silently and is only noticed when it fails is not a reliable job — it is a job that has already failed silently at least once and nobody knew. Cron reports success and failure to no one by default, so the nightly backup, the invoice run, and the data sync all share a property that should alarm you: their failure is invisible until a human happens to look for the output that is missing.
The two failure modes cron ignores
A scheduled job can fail in two distinct ways, and the default setup catches neither.
It can run and error — the script starts, hits a problem, and exits non-zero. Cron will email the output to the local mail spool, which on a modern cloud host goes to a file nobody reads. The error happened, it was even recorded, and it reached no human.
Worse, it can fail to run at all — the host was rebooting at the scheduled time, the cron daemon was stopped, the disk was full, or someone commented out the line during unrelated work and forgot. A job that does not run produces no output and no error, so the absence is the only signal, and absence is exactly what monitoring built around output cannot see.
The pattern: a dead-man's switch
The fix for the harder failure mode is to invert the logic. Instead of alerting when the job reports a problem, alert when the job fails to report success. The job pings a monitoring service at the end of a successful run; the service alerts you when an expected ping does not arrive within a window you define.
#!/usr/bin/env bash
set -euo pipefail
/srv/app/bin/nightly-export # the real work; -e aborts on failure
curl -fsS -m 10 --retry 3 https://checks.example.com/ping/nightly-export
The curl only runs if the export above it succeeded, because set -e aborts the script on any error. So the ping is not "the job started" — it is "the job finished correctly". If the export fails, or the host never runs the script at all, no ping arrives, and the monitoring service raises the alert you actually want.
Several hosted services do exactly this for a few dollars a month, and it is straightforward to self-host. The value is entirely in the inverted logic: you are monitoring for the absence of success, which is the only way to catch the job that never ran.
Capturing errors when the job does run
For the run-and-error case, capture the output and route it somewhere a human sees:
0 2 * * * /srv/app/nightly.sh >> /var/log/nightly.log 2>&1 || \
curl -fsS -m 10 -d "nightly export failed, see host" \
https://alerts.example.com/notify
The 2>&1 captures both standard output and errors to the log, and the || curl fires an alert on non-zero exit. Now a job that runs and fails tells you immediately, with the log available for the detail. Combined with the dead-man's switch above, you have covered both failure modes: the ping catches silent non-execution, the alert catches loud errors.
The subtler failures worth catching
Two conditions are technically successes but practically failures, and they are worth building in.
The job that runs too long. A backup that normally takes 20 minutes and now takes 3 hours is failing, even if it eventually succeeds — something has gone wrong with its inputs or the system. A monitoring service that knows the expected duration flags this; so does a simple timeout wrapper that treats overrun as an error.
The job that succeeds but does nothing. An export that completes and writes an empty file has failed at its purpose while reporting success. Assert on the result, not just the exit code: check that the output file is non-empty, that the row count is plausible, that today's date appears in it. The same principle as verifying a backup by restoring it — a clean exit is a claim, not a confirmation.
Move the schedule where you can see it
Cron itself is opaque: the schedule lives in files scattered across users and hosts, and there is no central view of what is supposed to run. Where the platform allows, prefer systemd timers, which put the schedule, the last run, the exit status, and the logs in one queryable place:
systemctl list-timers --all # every scheduled unit, next and last run
journalctl -u nightly-export.service # this job's history and output
This does not replace the alerting — you still need the dead-man's switch for the job that fails to run — but it removes the second problem of scheduled work, which is not knowing what is scheduled in the first place.
The test
Pick your most important scheduled job and break it on purpose in a safe environment: rename a file it needs, or stop it from running tonight. If an alert reaches a human within the window you would consider acceptable, your monitoring works. If nothing happens and the only way you would ever learn is by noticing the missing output days later, then that job has been running on trust, and trust is not a monitoring strategy for the thing that generates your invoices.