Cron Job Monitoring: From Crontab to Monitors
Cron job monitoring starts with your crontab. Here is how to turn five schedule fields into an expected period, plus the DST trap that breaks them.
Cron job monitoring starts with a file you already have. Open the crontab, read the five schedule fields, and turn each line into an expected period a monitor can measure against. That translation is where most setups go wrong. A schedule and an interval are not the same thing, and several common cron expressions have no single interval at all.
Start from the crontab, not from the tool
Cron job monitoring has an inventory step before it has a configuration step. Run crontab -l on every host that runs anything, then check /etc/cron.d, /etc/crontab and the per-user files under /var/spool/cron. Jobs hide in all four. Anything installed by a package or a configuration management run will be in the second or third, not in the one you edit by hand.
Write the inventory down before you create a single monitor. Three columns are enough: the schedule expression, what breaks if the job does not run, and who owns it. The third column is the one that decides whether an alert goes anywhere useful.
Most teams find jobs nobody claims. In our testing on a mid-sized deployment, roughly a fifth of the crontab entries had no owner anyone could name. Two of those would have caused a billing problem. Coverage is an inventory problem before it is a tooling problem.
Cron job monitoring starts with an expected period
The mechanism itself is inbound: the job calls a URL when it finishes, and silence past the deadline raises an alert. That part is covered in heartbeat monitoring. What matters here is the number you feed it.
| Cron expression | Reads as | Expected period | Watch out for |
|---|---|---|---|
*/15 * * * * | Every 15 minutes | 900s | Runs at :00, :15, :30, :45, not 15 minutes after the last run |
0 * * * * | Hourly on the hour | 3,600s | Nothing |
30 2 * * * | 02:30 daily | 86,400s | Local time, see the DST section |
0 9-17 * * 1-5 | Hourly, business hours | Uneven | 16 hours overnight, 64 hours over a weekend |
0 0 1 * * | First of the month | Uneven | 28 to 31 days |
0 3 * * 0 | 03:00 Sunday | 604,800s | One missed run means a week of silence |
Two of those six have no single period. For the business-hours job you have two options. Set the expected period to the longest legitimate gap, which makes the check useless on a Tuesday afternoon, or split it into two monitors. We prefer the split, because a check that tolerates 64 hours of silence is not really cron job monitoring at all.
The step syntax trips people up in a second way. */15 means minutes divisible by 15, not "fifteen minutes after the previous run". A job that starts at 14:14 and takes two minutes has its next start at 14:15, one minute later.
Is it a late run or a missed run?
A silent monitor cannot tell the difference, and that matters more than it sounds. A job that started on time and is still grinding away looks identical to a job that never fired, because neither has sent anything.
Fix it by signalling twice. Send a start signal when the job begins and a success signal when it finishes. Now three states exist: started and finished is healthy, started and never finished is a hang, and never started is a scheduling failure. Those three have completely different causes and completely different fixes.
#!/usr/bin/env bash
set -euo pipefail
HB="https://uptimeroom.com/api/heartbeat/<token>"
curl -fsS --retry 3 "$HB/start" > /dev/null
python3 /srv/reports/build_monthly.py
curl -fsS --retry 3 "$HB" > /dev/null
Without the start signal you will eventually widen the grace period to cover the worst run you have ever seen. By then the check only detects a job that has been dead for hours.
Overlapping runs and the lock that hides them
Cron fires on schedule regardless of whether the previous run finished. Two copies of an import job writing the same table is a data problem, so most people wrap the command in flock:
*/5 * * * * /usr/bin/flock -n /tmp/import.lock /srv/bin/import.sh
Then the monitoring gets confusing. When flock -n finds the lock held, it exits without running anything, so no signal is sent. The monitor treats that as a missed run and alerts, even though the system behaved exactly as designed.
Decide which event you actually care about. If a skipped run is acceptable, send the signal from the wrapper so a skip still counts as a check-in. If a skipped run means you are falling behind, let the alert fire. A job that cannot keep up with its own schedule is worth a ticket.
The timezone trap nobody catches in review
Cron follows the system clock, and the system clock usually follows a local timezone with daylight saving. The crontab(5) manual documents the behaviour. Jobs scheduled inside an hour that is skipped in spring do not run, and jobs inside a repeated autumn hour can run twice.
A 02:30 daily job on a server set to a European or North American timezone therefore fails once a year and double-runs once a year. Neither shows up in testing, and both produce a monitoring alert that looks like a bug in the monitor.
Two defences. Run servers in UTC, which removes the problem entirely and is what we would pick for anything new. Where that is not possible, set CRON_TZ=UTC at the top of the crontab so the schedule is evaluated against a clock that never jumps.
systemd timers and Kubernetes CronJobs
Cron is not the only scheduler, and the other two break assumptions you carried over. Cron job monitoring has to account for both.
A systemd timer with Persistent=true runs a missed job at the next boot, which can arrive hours late and still be correct. RandomizedDelaySec deliberately spreads start times, so your expected window has to include that spread. The systemd.timer documentation lists both options.
A Kubernetes CronJob adds concurrencyPolicy, which can be Allow, Forbid or Replace, and startingDeadlineSeconds. The controller also stops scheduling entirely if it misses more than 100 schedules, which the CronJob documentation spells out. A cluster that was down over a weekend can therefore come back with a job that never runs again and never errors.
Nightly batch windows and planned migrations belong in maintenance windows so a predictable gap does not page anyone.
What the free tiers cover
Comparing free tiers matters here, because scheduled jobs outnumber web endpoints in most estates and the counts run out quickly.
| Service | Free allowance as of September 2026 |
|---|---|
| Healthchecks.io | 20 checks, open source and self-hostable |
| Uptime Room | 10 monitors of any of the ten types, 1-minute interval |
The honest reading is that a pure cron monitoring tool gives you more free job slots. A general monitoring service lets the same 10 slots cover endpoints, certificates and DNS as well as jobs. If every one of your 40 cron entries needs its own check, the specialist wins on count. If you have eight jobs and a website, one tool is simpler. Monitor fields are documented in the REST API reference.
Common mistakes
One monitor for a schedule that has two rhythms. Business-hours jobs need two windows or two monitors. A single period wide enough for the weekend detects nothing during the week.
Assuming the container clock matches the host. A container without timezone data runs UTC while the host runs local time, and the schedule silently shifts.
Alerting every failure to the same channel. Job failures are usually a ticket, not a page. Mixing them with outage alerts is how a channel gets muted, as covered in alert fatigue.
Forgetting to monitor cron jobs that run rarely. A quarterly job is exactly the one that will have rotted, and the failure surfaces three months after somebody changed a path.
Good cron job monitoring is mostly bookkeeping. Inventory every scheduler on every host, set an honest expected period per line, send a start signal as well as a success signal, and run servers in UTC. Do those four and the alerts become trustworthy. The free plan covers 10 monitors at a 1-minute check interval as of September 2026, which is enough to cover the jobs that would cost you money.
FAQ
It runs in reverse compared with a website check. Your scheduled task sends an HTTP request to a unique URL when it finishes, and the monitor raises an alert if that request does not arrive within the expected period plus a grace window. Because the server reaches out, the pattern works from behind a firewall with no inbound access.
Read the five crontab fields and work out the longest legitimate gap between runs. Simple expressions map cleanly: hourly is 3,600 seconds, daily is 86,400. Business-hours and monthly schedules have no single period, so split them into separate monitors with different windows rather than widening one check until it detects nothing.
Nothing replaced cron, but two schedulers now sit alongside it. systemd timers add persistence across reboots and a randomised start delay. Kubernetes CronJobs add a concurrency policy and a starting deadline. Both change the timing assumptions a monitor relies on, so check which scheduler actually runs a job before setting its expected window.
Send two signals instead of one. A start signal when the job begins and a success signal when it completes give you three distinguishable states: healthy, hung after starting, and never scheduled. With only the success signal, a job still running and a job that never fired look identical, because neither has sent anything.
Healthchecks.io offers 20 free checks and can be self-hosted, which suits an estate that is mostly scheduled jobs. A general monitoring service covers fewer job slots but uses the same allowance for endpoints, certificates and DNS. Pick by ratio: many jobs and few endpoints favours the specialist tool.
Related reading
- Heartbeat Monitoring: Catch Jobs That Never Ran - Heartbeat monitoring waits for your cron job to check in, then alerts when the signal never arrives. Here is how to size the grace period.
- Alert Fatigue: How to Cut Your Page Count - Alert fatigue is a volume problem with a number attached. Count pages per responder per week, then use four levers to cut the ones nobody acts on.
- Maintenance Window: Plan One Without False Alerts - A maintenance window is announced downtime you control. Here is how to size it, suppress the right alerts, and keep the data you will need later.