Skip to content
Monitoring

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.

12 min read 2026-07-03

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:

  1. Ping URL: The monitoring provides a unique URL for each job. The job calls it at the end via HTTP — a single curl is enough.
  2. Expectation window: From the job's schedule (cron expression or "every 15 minutes"), the monitoring calculates when the next ping is due.
  3. 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 patternMeaning
No start pingJob never started (cron daemon, crontab, server)
Start ping without end pingJob started but crashed or hangs
Start + end, but excessive durationJob 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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):

ToolFreePaid from
LIVCKSelf-hosted EUR 0 (commercial use allowed) · Cloud Free: 20 services (public beta)Self-hosted license from EUR 9.90/month
Healthchecks.io20 checks, 3 team membersBusiness $20/month (100 checks)
Cronitor5 monitors, 5-min interval$2/monitor + $5/user
Dead Man's Snitch1 snitch, standard intervals only$5/month (3 snitches)
Sentry Crons1 cron monitor$0.78/monitor (paid plans only)
UptimeRobotHeartbeat on free plan, 5-min intervalPro for 1-min checks
Better Stack10 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.

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.