How to monitor a cron job and get alerted when it does not run
Give each scheduled job a URL to call when it finishes, and get an alert when the call is late, missing, or reports a failure.
Catch silent failures · 5 min read · Updated Oct 12, 2026
A cron job that stops does not tell anyone. The backup script that lost its credentials three weeks ago, the invoice run on a server that was rebuilt without its crontab: nothing fails loudly, because nothing runs at all. You find out when you need the backup.
An uptime check cannot see this, because there is no address to check. What works is the opposite arrangement: the job reports in, and silence is the alarm.
How it works
You give the job a secret URL. The last thing the job does on every run is call it. A service on the other end knows how often the call should arrive. When one does not arrive in time, it alerts you.
This is usually called a heartbeat, a check-in, or a dead man's switch. It catches every way a job can go quiet with one mechanism: the server is off, cron is not running, the crontab was overwritten, the script hangs, or the script exits early.
It works the same for anything that runs on a schedule: a crontab entry, a systemd timer, a Kubernetes CronJob, a scheduled CI workflow, a queue worker that should finish a batch every hour.
What the watcher needs
Any heartbeat service will do, or one you run yourself. What matters is that it has these four things:
- An interval and a grace time per job. The interval is how often the job runs. The grace time is how late it may be before anyone is told.
- A way to report failure, not only silence. A job that ran and failed should alert at once, not after the next run is missed.
- Alerts that reach a person. A dashboard nobody opens is not monitoring.
- A home outside the machine it watches. A watcher on the same server stops when the server does.
That last point is why a second cron job that checks the first is not enough. It is fine as a first step, but something still has to notice when the whole machine is gone.
Step 1: Decide what "on time" means
The job pings when it finishes, so the time between two pings is never exactly the interval. A backup that takes 20 minutes one night and 50 the next pings half an hour "late" without anything being wrong.
Grace absorbs that. A workable rule: set it to the longest the job has ever taken, plus a margin. For a nightly backup that normally runs under an hour, three hours is reasonable. For a job that runs every five minutes and takes seconds, three minutes is enough.
Too short and you get alerts for slow nights. Too long and a dead job goes unnoticed for that much longer.
Step 2: Call the URL at the end of the job
In the crontab, run the job and then call the URL, joined with && so that the call only happens when the job succeeded:
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -o /dev/null -m 10 --retry 3 https://hb.example.com/ping/YOUR_TOKEN
A failed job then sends nothing, and you are alerted when the grace time runs out.
The curl options matter: -m 10 limits each attempt to ten seconds, so a slow network cannot hang the job, --retry 3 rides out a brief failure, and -fsS -o /dev/null keeps it silent unless something goes wrong, so cron has nothing to mail you.
Many services can also be told the result, so that a failure alerts at once instead of after the wait. How differs from one to the next: the job's exit code or a word like fail on the end of the URL, or a parameter. Check yours before copying the lines below, which use the exit-code form. Join the commands with ; so the call always happens, and pass $?, the exit code of the command before it:
0 3 * * * /usr/local/bin/backup.sh; curl -fsS -o /dev/null -m 10 --retry 3 https://hb.example.com/ping/YOUR_TOKEN/$?
For a systemd timer, put both commands in a small script and point the service's ExecStart at it:
#!/bin/sh
/usr/local/bin/backup.sh
status=$?
curl -fsS -o /dev/null -m 10 --retry 3 "https://hb.example.com/ping/YOUR_TOKEN/$status"
exit $status
For a scheduled GitHub Actions workflow, store the token as a repository secret and add a last step that always runs:
- name: Report the result
if: always()
run: curl -fsS -o /dev/null -m 10 --retry 3 "https://hb.example.com/ping/${{ secrets.HEARTBEAT_TOKEN }}/${{ job.status == 'success' && '0' || '1' }}"
Step 3: Test the alarm, not the ping
Seeing a ping arrive proves half of the setup. The half that matters is what happens when one does not.
- Run the
curlline by hand. The job shows as on time. - Report a failure by hand, or stop the job and wait out the interval and the grace time. An alert should reach you, on the device where you would want it at 3 a.m.
- Send a good ping again. The alert should clear by itself.
If step 2 produced nothing, the monitoring is decoration. Fix where alerts go before trusting it.
Common mistakes
- Pinging at the start of the job. That proves cron started it, not that it finished. Ping at the end.
- Leaving
curlwithout a time limit. A network problem can then hold the job open. Keep-m 10. - One URL for several jobs. If three jobs share it, two can die and the third keeps it green. One URL per job.
- Committing the URL to a public repository. Anyone with it can send pings. Treat it as a secret, and replace it if it leaks.
- Alerts that go to a channel nobody watches at night. Decide who is told, and how, before the first failure.
- Never testing the failure. A heartbeat that has only ever been green has not been tested.
Doing this in saysOK
saysOK calls these heartbeats, and they are included in every plan. A heartbeat has the Interval and Grace from step 1, and a ping URL to put at the end of the job. The URL takes the exit code on its end, as in step 2, and a code other than 0 opens an incident at once.
A heartbeat is On time, then Late once the interval has passed, and Missed when the grace time runs out, which is when the channels you chose are told. A new heartbeat shows Waiting until its first ping and is never marked missed before then, so send the first one by hand. To try it, start free: sign-up goes straight to a heartbeat and shows its first ping as it arrives.
Next
See pricing for what each plan includes. If the job produces something customers see, pair it with a check from outside: get alerted before an SSL certificate expires, or check that a login or checkout flow still works.
Set this up in saysOK
Start on the free plan. The guide says where a step needs a paid one.