You own scheduled jobs: backups, reports, syncs, certificate renewals. The worst failure is the one that makes no noise: the job stops running and nothing says so. This post explains why that happens, reviews four ways to monitor cron jobs and what each misses, and shows how to set up a heartbeat with copyable crontab and systemd lines.
What exactly is a cron job?
A cron job is a command that the cron daemon on a Unix-like system runs on a schedule. The schedule is a line in a crontab with five time fields (minute, hour, day of month, month, day of week) followed by the command.
30 2 * * * /usr/local/bin/backup.sh
This line runs the backup at 02:30 every day.
systemd timers, Kubernetes CronJobs and cloud schedulers do the same job, and the rest of this post applies to them too.
Is cron outdated?
Cron is not outdated. It dates from the 1970s and is still installed on almost every Linux server. Newer schedulers such as systemd timers, Kubernetes CronJobs and AWS EventBridge Scheduler add features. All of them share the same weakness: when a scheduled job does not run, nothing tells you.
Why cron jobs fail silently
Ordinary monitoring raises an alert when something bad happens. A job that did not run produces no event at all. There is no error, no log line, and no metric.
It happens in several ways:
- The server was rebuilt or replaced and the crontab was not carried over.
- The job was commented out during maintenance and never restored.
- The script fails at once because of an expired credential, a full disk or a changed path.
- Cron tries to email the output and no mail is set up on the host.
- The job hangs and never finishes.
- The previous run is still going and a lock file blocks the new one.
- A timezone or daylight saving change moved or skipped the run.
- The container or node that ran it was scaled away.
People find out later: backups that stopped weeks ago, invoices that did not go out, certificates that did not renew, data syncs that went stale.
Four ways to monitor cron jobs
There are four common methods, and each misses something.
| Method | How it works | What it misses |
|---|---|---|
| Email from cron (MAILTO) | Cron emails any output of the job | Needs working mail on the host; tells you about errors, not about a job that never ran; easy to ignore |
| Log or exit-code alerts | The job logs a failure or a metric and the monitoring alerts on it | A job that never started logs nothing |
| Check the result | A separate check looks at the output, for example the age of the newest backup file | Reliable, but you write one check per job |
| Heartbeat (dead man's switch) | The job pings a URL when it succeeds; an alert fires when a ping is late | The monitor must run somewhere other than the host it watches |
The first two methods only catch failures that produce output. If the job never starts, email sends nothing and logs stay empty. Checking the result works, but you must write and maintain a separate check for every job.
Heartbeat monitoring covers the no-event case. Many teams use exit-code alerts and a heartbeat together: the exit code says why a job failed, and the heartbeat catches the job that never ran.
How heartbeat monitoring works
Heartbeat monitoring, also called a dead man's switch or check-in monitoring, turns the logic round: the job reports success, and silence is the alarm.
It catches every failure in the earlier list because all of them end with no ping. A job that never started, a script that failed, or a host that went down all produce silence.
The monitor must run somewhere other than the host it watches, so that a host that dies takes only the job with it and the late ping still raises an alert.
How to set up cron job monitoring with a heartbeat
Create a check with an expected interval.
Set the interval a little longer than the schedule so a slow run does not raise an alert. A daily job needs 25 hours rather than 24. A job every 5 minutes needs 7 minutes.
Add the ping to the end of the job so it runs only on success.
For crontab:
# crontab: ping only when the job succeeds 30 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 'YOUR_PING_URL' > /dev/nullFor systemd:
# systemd service ExecStart=/bin/sh -c '/usr/local/bin/backup.sh && curl -fsS -m 10 --retry 3 YOUR_PING_URL'What the curl flags do:
-ffails on an HTTP error,-sSis quiet but still shows errors,-m 10gives up after 10 seconds so the ping cannot hang the job, and--retry 3retries a failed ping. With&&, a failed job skips the ping. For a Kubernetes CronJob, put the same&&curl at the end of the container command.Decide who is notified when the ping is late.
Test it: disable the job once and watch the alert arrive.
Cron job monitoring best practices
- Ping on success only. A ping at the start of the job still arrives when the job then hangs or fails.
- Use one heartbeat per job. Name it after the job, such as "Nightly database backup", so the alert says what is down.
- Treat the ping URL as a secret. Do not commit it to version control.
- Set the interval slightly longer than the schedule. A daily job with a 25-hour interval tolerates a slow run.
- Send late-ping alerts to a person. Route them through an escalation policy, not only to a channel, so someone owns the incident.
- Match severity to the job. A missed backup can wait for working hours, but a missed payment run cannot.
- A heartbeat proves the job finished, not that the output is right. For backups, also test a restore from time to time.
Cron job monitoring tools
Dedicated services include Healthchecks.io (open source, can be self-hosted), Cronitor, and the cron monitoring in Sentry and UptimeRobot. If you already run an on-call tool, a heartbeat there puts the missed job straight onto the same escalation policy as other alerts.
Heartbeats in OnCallAlerting
An OnCallAlerting heartbeat has a name, an interval from 1 minute to 30 days, an escalation policy, and a severity (Critical, Warning, or Info; Warning is the default). Creating it shows a ping URL once. Treat it like a password, and rotate it if you lose it.
The job sends a GET or POST to the ping URL. No headers or body are needed. Pings are limited to 60 per minute per heartbeat. A new heartbeat waits for its first ping and cannot go down before it.
When a ping is late, the heartbeat is marked Down. One incident opens on the escalation policy with the title "Heartbeat missed:" followed by the name. A heartbeat that stays down opens no further incidents. The next ping marks it Up and resolves the incident.
OnCallAlerting does not run the job, read its logs, or check its output. Notifications are Slack and in-app only. Setup is in the heartbeats docs and the escalation policies docs. To try it, start a 30-day trial.