Uptime checks are all built the same way. Something reaches out to your service, your service answers, and the answer is judged. That model covers anything with an address: a website, an API, a mail server, a database port.
It cannot see a cron job. A nightly backup has no port to knock on. When it stops running there is nothing to fail, nothing to time out, and nothing to alert on. The job simply stops, cron stops mailing anyone because that was turned off years ago, and the absence of a backup looks exactly like a backup that worked.
That is usually discovered on the day the backup is needed.
Inverting the check
A heartbeat monitor turns the direction around. Instead of relay19 calling your service, your job calls relay19 when it finishes. Each heartbeat gets a URL with its own token, plus an expected period. A GET or a POST both count. If a ping arrives on time, all is well. If the window passes with no ping, an incident opens.
The job does not have to report failures for this to work. It only has to report success. Anything that stops the job from finishing, a crash, a full disk, a syntax error in the wrapper script, a machine that never came back after a reboot, a cron entry someone commented out during an unrelated change, produces the same signal: silence where a ping should have been.
Adding one to an existing job
The ping is a plain HTTPS request at the end of the command, and the &&
matters:
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 \
https://p.relay19.com/v1/hb/<your-token>
With &&, the ping only fires when the backup exits zero. A backup that
fails now produces silence, and silence is what opens the incident. Swap that
for ; and you have built something worse than no monitoring at all: a
heartbeat that reports success every night regardless of what the job did.
-f makes curl fail on an HTTP error instead of quietly writing the error
body to stdout, -m 10 stops a hung ping from holding the cron slot open,
and --retry 3 rides out a momentary network problem so a healthy job does
not page you over a dropped packet.
Choosing the period and the grace
Two numbers define a heartbeat: how often the ping is expected, and how late it may be before that counts as missing.
The period is simply the job’s schedule. The grace is the part people get wrong. Set it too tight and every slow night pages you. Set it to a day and a daily backup can miss an entire run before anyone hears about it.
A reasonable starting point is the longest the job has ever legitimately taken, plus a comfortable margin:
nightly backup, usually 40 min, worst 90 min period 24h grace 3h
hourly sync, usually 2 min period 1h grace 15m
weekly report, Monday 07:00 period 7d grace 6h
If the job’s duration varies with your data, the worst case grows over time. It is worth revisiting the grace once a quarter on anything that scales with the size of the database.
Jobs worth a heartbeat
The rule of thumb: anything whose failure is invisible until you need the result. In practice that is a short list, and it is nearly the same list at every company.
- Database backups, and separately, the restore test that proves they work.
- Certificate renewal, which fails quietly and surfaces as an outage months later.
- Data exports and imports on a schedule, especially ones a partner depends on.
- Cache warming, search index rebuilds, sitemap generation.
- Queue workers, pinged from a periodic task inside the worker itself.
- Log rotation and cleanup jobs, whose failure shows up as a full disk at an inconvenient hour.
The point is the silence
Everything else in a monitoring system watches for something bad happening. Heartbeats watch for nothing happening, which is the failure mode that survives longest in production precisely because it produces no evidence.
Heartbeats are included on every plan, including the free one. The quickstart walks through creating one and wiring it into a cron entry, and it takes about two minutes for the job you are most afraid to lose.