Cron Job Monitoring: Heartbeat Monitoring in Practice
Monitor cron jobs with heartbeat monitoring: how the dead man's switch works, typical failure modes and best practices for cron, systemd and Kubernetes.
Why cron jobs fail silently
A crashed web server gets noticed immediately: visitors see error pages, uptime monitoring fires an alert, support tickets roll in. A cron job that stopped running gets noticed by nobody — until it is too late.
The nightly backup, the invoice run on the first of the month, the queue worker, the import from your ERP system: none of these jobs have a port, a URL, or users who complain. When they fail, no error appears — simply nothing happens. And that is exactly what makes them dangerous:
- A backup script that has not run for three months gets noticed the moment you need the backup.
- A stuck invoice run only surfaces when customers call asking where their invoice went.
- A stopped queue worker silently piles up thousands of jobs until memory runs out.
Classic monitoring does not help here. An HTTP check can verify that your website responds — but no monitoring system in the world can see from the outside whether a script on your server ran last night. The solution is to invert the principle: the monitoring does not ask the job — the job reports to the monitoring.
How heartbeat monitoring works
Heartbeat monitoring (also called a dead man's switch, after the safety device in a train cab) is built on a missing signal: the job sends a short sign of life after every successful run. If the sign of life stays out, an alert fires.
Ping URL and expectation window
The flow takes three steps to explain:
- Ping URL: The monitoring provides a unique URL for each job. The job calls it at the end via HTTP — a single
curlis enough. - Expectation window: From the job's schedule (cron expression or "every 15 minutes"), the monitoring calculates when the next ping is due.
- Alert: If the ping does not arrive within the window, the job counts as failed — and alerting kicks in.
In the crontab, it looks like this:
# Backup at 2:00 AM — ping ONLY on success
0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://monitoring.example.com/ping/a1b2c3
The && here is not a detail — it is the core of the whole setup. More on that in a moment.
Sizing the grace period
No job runs for exactly the same duration every time. To keep natural variance from triggering false alarms, there is the grace period: a buffer after the expected time before the alert fires.
The rule of thumb: base it on the maximum realistic runtime, not the average. If your backup normally takes 5 to 15 minutes, a grace period of 30 minutes is reasonable. Set it too tight and it produces false alarms and alert fatigue — set it too generously and it gives away reaction time. For jobs with highly variable runtimes, start pings plus a runtime limit beat an overstretched grace period.
Start and end pings
Advanced setups send two signals: one when the job starts, one at the end. That makes three states distinguishable that a single ping cannot separate:
| Signal pattern | Meaning |
|---|---|
| No start ping | Job never started (cron daemon, crontab, server) |
| Start ping without end ping | Job started but crashed or hangs |
| Start + end, but excessive duration | Job runs, but something slows it down |
As a bonus, the runtime becomes measurable — and a job that gets gradually slower stands out before it grows into the time window of its next run. Some services additionally accept an explicit fail ping with an exit code: the job then reports its failure immediately instead of surfacing only after the expectation window expires.
The four failure modes of a cron job
Cron jobs fail in four fundamentally different ways — and each needs a different detection mechanism:
- The job does not run at all. Server down, cron daemon stopped, crontab overwritten during a deployment, typo in the schedule. The treacherous part: no log entry is created, because nothing runs. This failure class is detectable only via a dead man's switch.
- The job hangs or runs too long. A hanging database query, a blocked mount, a lock that was never released. Detection: start ping without end ping, or a breached runtime limit.
- The job runs too often. A duplicated crontab entry, the same job on two servers, overlap with the previous run. Detection: pings outside the expected schedule.
- The job completes — but the result is wrong. The most dangerous case: exit code 0, ping sent, but the backup is a 0-byte file. No external monitoring helps here — the script itself must validate its result (file size, row count, checksum) and exit with an error when in doubt.
Best practices for reliable pings
&& instead of semicolon
The single most important tip in this guide. These two lines look almost identical and separate a working monitoring setup from a useless one:
# CORRECT: ping only if backup.sh exits with 0
backup.sh && curl -fsS https://monitoring.example.com/ping/a1b2c3
# WRONG: ping is ALWAYS sent — even if backup.sh fails
backup.sh ; curl -fsS https://monitoring.example.com/ping/a1b2c3
With the semicolon, the job reports success even when the backup failed. The monitoring shows permanent green — which is worse than no monitoring at all, because it creates false confidence.
Calling curl correctly
The ping itself must neither stall nor break the job. The proven flag combination:
curl -fsS -m 10 --retry 5 -o /dev/null https://monitoring.example.com/ping/a1b2c3
-m 10— timeout: the ping never hangs longer than 10 seconds, even if the monitoring is unreachable.--retry 5— transient network errors get absorbed instead of producing a false alarm.-fsS— silent on success, but errors stay visible in the log.-o /dev/null— the response body does not matter, only the delivery.
Preventing overlaps and hangs
Two small standard tools solve two big problem classes:
# flock: prevents a new run from starting while the old one is still running
0 * * * * flock -n /tmp/import.lock /usr/local/bin/import.sh && curl -fsS https://…/ping/import
# timeout: a job can never hang forever (exit 124 after 25 minutes)
0 2 * * * timeout 25m /usr/local/bin/backup.sh && curl -fsS https://…/ping/backup
Keeping exit codes honest
Bash scripts like to mask errors — a pipe such as pg_dump | gzip returns the exit code of gzip, even if pg_dump failed. Three lines at the top of the script restore order:
#!/usr/bin/env bash
set -euo pipefail
With this, the script aborts on every error, every unset variable and every pipe failure — and the success ping behind the && stays out, exactly as it should.
systemd timers and Kubernetes CronJobs
systemd timers are the successor to classic cron jobs on modern Linux systems and bring more to the table out of the box: logs via journalctl, Persistent=true to catch up on missed runs, and OnFailure= hooks for local reactions. What they cannot do: alert externally. If the whole server goes down, a local hook reports nothing. The external heartbeat remains mandatory here too — cleanly wired up via ExecStartPost=, which only executes after a successful run.
Kubernetes CronJobs have failure classes that classic cron does not know: image pull errors, pods stuck in Pending forever due to missing resources, missed startingDeadlineSeconds, or a badly chosen concurrencyPolicy. kubectl get cronjob only shows the last status — Kubernetes provides neither history nor alerting. The simplest robust approach is the same as everywhere: a && curl … ping at the end of the container command, evaluated by an external heartbeat monitoring service.
Comparing the tools
The niche is well populated — an honest look at the free entry points (as of July 2026):
| Tool | Free | Paid from |
|---|---|---|
| LIVCK | Self-hosted EUR 0 (commercial use allowed) · Cloud Free: 20 services (public beta) | Self-hosted license from EUR 9.90/month |
| Healthchecks.io | 20 checks, 3 team members | Business $20/month (100 checks) |
| Cronitor | 5 monitors, 5-min interval | $2/monitor + $5/user |
| Dead Man's Snitch | 1 snitch, standard intervals only | $5/month (3 snitches) |
| Sentry Crons | 1 cron monitor | $0.78/monitor (paid plans only) |
| UptimeRobot | Heartbeat on free plan, 5-min interval | Pro for 1-min checks |
| Better Stack | 10 heartbeats, 3-min interval | +$17/month per additional 10 |
Worth noting in the fine print: Healthchecks.io is open source and self-hostable — the classic choice for the frugal, but a pure cron job tool without uptime monitoring or a statuspage. Cronitor is the most mature at pure cron job monitoring, but bills per monitor and per user — growing setups get expensive fast. And UptimeRobot's free plan only covers non-commercial use.
Cron job monitoring with LIVCK
LIVCK ships heartbeat monitoring as one of seven check types — alongside HTTP(S), TCP, ping, DNS, SSL and Manual/API. The schedule accepts simple intervals or real cron expressions with time zones, and via the /start and /fail signals plus exit codes, LIVCK tells "never started" apart from "crashed" — including runtime measurement and a maximum-runtime guard against hanging jobs. If the expected signal stays out, alerting fires via email, SMS, voice call, Slack, Microsoft Teams, Telegram or Discord.
The real difference from the specialists lies in the complete package: with LIVCK, a failed heartbeat is not just an internal alert — it can flow directly into the statuspage and incident management, turning a silent cron job failure into transparent communication in a single tool. For operation, the choice is yours: self-hosted via Docker Compose (EUR 0, commercial use allowed — runs on a Hetzner server from under EUR 5/month), a managed service on German servers, or LIVCK Cloud with a free plan (20 services) — public beta, access via the waitlist. Since LIVCK is built in Germany and works GDPR by design, your monitoring data stays where it belongs.
Especially after the Freshping shutdown, the self-hosted approach is more than a matter of taste: no vendor can switch off monitoring that runs on your own server.
Conclusion
Cron jobs are the quietest failures in any infrastructure: no port, no error message, no angry user — just a job that at some point simply stops running. Heartbeat monitoring inverts the principle and turns the absence itself into the alert signal.
The essence of this guide: send the ping only on success (&&, never ;), harden curl with timeout and retries, size the grace period on maximum rather than average runtime, prevent overlaps with flock and hangs with timeout — and keep scripts honest with set -euo pipefail. If you add start pings, you can tell "never started" from "crashed" and see runtime trends before they become a problem.
And just like with uptime monitoring, the same rule applies here: monitoring belongs on independent infrastructure — otherwise it goes down together with the very jobs it is supposed to watch.
Related articles
Incident Management: Process, Roles and Communication
Incident management explained: lifecycle, severity levels, roles, escalation, post-mortems and metrics like MTTR — plus professional incident communication.
Self-HostingSelf-Hosted Statuspage — Why, How and Which Solution?
Self-hosted vs. cloud statuspage: benefits, requirements, Docker setup and comparison with open-source alternatives like Uptime Kuma and CachetHQ.
MonitoringUptime Monitoring: Fundamentals, Check Types and Choosing the Right Tool
Uptime monitoring explained: check types (HTTP, TCP, heartbeat), intervals, alerting and how to choose a tool — including self-hosting and GDPR.
Related comparisons
Ready for your own statuspage?
LIVCK: Monitoring and statuspage from Germany. Self-hosted, managed, or cloud. GDPR by design.
No credit card required. No license fees.