← Knowledge hub
Guideby Uptime Room· September 24, 2026· 7 min read

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 expressionReads asExpected periodWatch out for
*/15 * * * *Every 15 minutes900sRuns at :00, :15, :30, :45, not 15 minutes after the last run
0 * * * *Hourly on the hour3,600sNothing
30 2 * * *02:30 daily86,400sLocal time, see the DST section
0 9-17 * * 1-5Hourly, business hoursUneven16 hours overnight, 64 hours over a weekend
0 0 1 * *First of the monthUneven28 to 31 days
0 3 * * 003:00 Sunday604,800sOne 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.

ServiceFree allowance as of September 2026
Healthchecks.io20 checks, open source and self-hostable
Uptime Room10 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

How does cron job monitoring work?

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.

How do I set the expected period for a cron schedule?

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.

What replaced cron jobs?

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.

How do I tell a late job from a job that never started?

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.

What is the best free way to monitor cron jobs?

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