A cron job that stops running doesn't throw an error. It just stops. There's no failed request, no 500 status code, no dashboard turning red — the backup script, the report generator, the cleanup task simply doesn't run tonight, and nothing about that is visible from the outside. Cron job monitoring exists to close that gap.
What Is Cron Job Monitoring?#
Cron job monitoring confirms that a scheduled task — a cron job, a Kubernetes CronJob, a Windows Task Scheduler entry, a queued background worker — actually executed, executed on time, and finished without error. It's a different problem from monitoring a website or an API, because there's no request coming in to check against.
Standard uptime monitoring works by sending a request and grading the response: is the server up, did it answer within the timeout, did it return the right status code. That model assumes something is always listening. A cron job isn't listening for anything. It wakes up on a schedule, does its work, and exits. There's nothing to poll in between runs, and nothing to poll if a run silently never happens.
Why Status Checks Don't Work for Cron#
If you try to monitor a cron job the same way you'd monitor a website, you run into the core mismatch immediately: a cron job that fails to run produces zero signal. No 500, no timeout, no connection refused. The absence of an event is the failure, and a monitor built to check for the presence of a bad response has nothing to grab onto.
This is why cron monitoring uses the opposite direction of check.
How Heartbeat Monitoring Works#
A heartbeat check (also called a dead man's switch) flips who initiates the request. Instead of a monitoring service reaching out to your job, your job reaches out to the monitoring service:
1. Cron job runs, does its work2. Job finishes successfully3. Job sends a GET or POST to a unique heartbeat URL4. Monitoring service resets a timer for that heartbeat
1. Cron job runs, does its work2. Job finishes successfully3. Job sends a GET or POST to a unique heartbeat URL4. Monitoring service resets a timer for that heartbeat
The check is defined by two numbers:
- Period — how often the job is expected to run (daily, hourly, every 15 minutes).
- Grace — how much lateness is tolerated before it's treated as a failure, to absorb normal variance in run time.
If the next ping doesn't arrive within period + grace, the monitor flips to overdue and fires an alert. No port has to be opened, no credentials handed to the monitoring service, and no outbound access from the monitor into your infrastructure — the job reports out on its own schedule, which is also why heartbeat checks work equally well for jobs running behind a firewall, on a laptop, or inside a Kubernetes cluster with no public ingress.
What Cron Monitoring Should Catch#
The job never ran. The most common failure: a crashed host, a disabled crontab entry, a Kubernetes CronJob stuck in a bad state, a deploy that silently dropped the schedule.
The job ran late. Resource contention, a slow dependency, or a queue backup delays the run past its grace window — worth knowing even if it eventually completes.
The job ran but errored out. This only gets caught if the heartbeat ping sits at the end of the script's success path, not the start. A ping placed before the job's logic runs fires regardless of whether the job actually succeeds, which silently defeats the monitor. Put the ping last, after the work is confirmed done, so a script that throws partway through never sends it — and gets flagged exactly like a job that never ran at all.
Setting Up Cron Job Monitoring#
1. Create the heartbeat check with a name, a period matching the job's schedule, and a grace window sized to the job's normal runtime variance, not just its average.
2. Get a unique ping URL from the monitoring platform. With ObserveOne's CLI:
obs heartbeat create --name "Daily Backup" --period 86400 --grace 3600
obs heartbeat create --name "Daily Backup" --period 86400 --grace 3600
This creates a daily heartbeat with a one-hour grace window. Creating it returns a unique ping URL for that check.
3. Add the ping as the last line of the job, after its success logic, not before:
#!/bin/bash./run-backup.sh && curl -fsS "$HEARTBEAT_PING_URL"
#!/bin/bash./run-backup.sh && curl -fsS "$HEARTBEAT_PING_URL"
The && matters — the ping only fires if run-backup.sh exits cleanly.
4. Route the alert. Background jobs (log rotation, cache warming) can go to a Slack channel. Anything customer-facing (billing runs, data exports) should page on-call the same way a site-down alert would.
5. Mute during planned changes. Pausing or muting the heartbeat before a deploy, a manual re-run, or a schedule change stops the fix itself from triggering the alert it exists to prevent:
obs heartbeat toggle-muted <id> # mute alerts without pausing the timerobs heartbeat reset <id> # reset the timer after a manual run
obs heartbeat toggle-muted <id> # mute alerts without pausing the timerobs heartbeat reset <id> # reset the timer after a manual run
Kubernetes CronJobs#
The same mismatch applies to Kubernetes CronJobs, with an extra failure mode on top: a CronJob can silently stop scheduling Job objects entirely if it hits startingDeadlineSeconds or gets stuck in a Suspend: true state after a botched change, and kubectl get cronjobs won't page anyone about it. A heartbeat ping added to the pod's entrypoint, sent only after the container's main process exits 0, catches both the "never scheduled" and the "scheduled but the container crashed" cases with one check.
Cron Monitoring vs. Uptime Monitoring#
| Uptime monitoring | Cron job monitoring | |
|---|---|---|
| Who initiates the check | Monitor polls your service | Your job pings the monitor |
| What triggers an alert | Bad response, timeout | No ping within period + grace |
| Works behind a firewall | No (needs inbound access) | Yes (job only needs outbound) |
| Catches | Server down, slow, wrong content | Job never ran, ran late, exited with an error |
Most teams need both: uptime checks for anything that serves a request, heartbeat checks for anything that runs on a schedule and doesn't.
Conclusion#
A cron job's failure mode is silence, not an error page, which is exactly what makes it easy to miss for weeks. Heartbeat monitoring closes that gap by flipping the check around: instead of asking "did the server answer," it asks "did the job report in on time," and pages you the moment it doesn't.
ObserveOne's heartbeat checks sit alongside HTTP, TCP, database, and SSL monitoring in one dashboard, with period and grace configurable per job through the CLI or the API. Build the schedule first with the free cron expression parser, then wire a heartbeat to it so a broken schedule and a broken run both get caught the same way.