---
title: Heartbeat & Cron Job Monitoring: the Dead Man's Switch Your Backups Deserve
description: Most monitoring checks that something is up. Heartbeat monitoring does the opposite — it alerts when something that should run goes silent. The concept, who needs it, and how to set it up so a dead backup job can't hide for three weeks.
canonical: https://watchfor.io/blog/heartbeat-cron-monitoring
---

# Heartbeat & Cron Job Monitoring: the Dead Man's Switch Your Backups Deserve

2026-07-30 · WatchFor Team · Monitoring · monitoring, basics, automation

Most monitoring checks that something is up. Heartbeat monitoring does the opposite — it alerts when something that should run goes silent. The concept, who needs it, and how to set it up so a dead backup job can't hide for three weeks.

Here's a failure mode regular [uptime monitoring](/blog/uptime-monitoring-101) can't catch: your nightly backup job silently stops running. No error page, no 500, no alert — it just quietly *doesn't happen*. You find out weeks later, at the worst possible moment: when you actually need the backup.

Catching the **absence** of an event needs a different approach. That approach is **heartbeat monitoring** — and if you run anything on a schedule, you probably need it more than you think.

## The problem: the invisible half of your infrastructure

Uptime monitoring works by *reaching out* — a prober requests your URL and checks the response. That covers everything with an endpoint: websites, APIs, mail servers.

But a big share of what keeps a business running has **no endpoint at all**:

- Nightly **backups** (database dumps, snapshots, offsite syncs)
- **Cron jobs** — cleanup, billing runs, report generation, certificate renewals
- **Queue workers** and consumers (Sidekiq, Celery, BullMQ)
- Data **pipelines** and ETL jobs
- Scheduled **syncs** with third parties
- **IoT devices** and edge boxes behind NAT

When one of these dies, there's nothing to ping. The failure is an *absence* — and absence is invisible to "is it up?" checks. Worse, these failures are **silent by nature**: a crashed worker throws no error page, a lost crontab entry writes no log. The only observable signal is that something which should have happened… didn't.

> **The core insight: some failures are the absence of success.** A dead backup job doesn't send an error — it sends nothing. Only by *expecting* a regular signal can you detect "this should have happened and didn't."

## What heartbeat monitoring is

Heartbeat monitoring (also called a **dead man's switch**, check-in monitoring, or passive monitoring) flips the direction. Instead of the monitor checking the job, the **job checks in with the monitor**:

```
Job runs successfully → it pings a unique URL → all good
Job didn't run / crashed / hung → no ping arrives → alert!
```

| Normal monitoring | Heartbeat monitoring |
| --- | --- |
| You ping the service | The job pings you |
| Alerts when it **responds wrong** | Alerts when it **goes silent** |
| For endpoints (sites, APIs) | For scheduled work (cron, backups, workers) |

The name comes from safety systems that trigger when an operator *stops* holding a lever. Same idea here: the alarm fires on **silence**, not on an error — which is exactly what catches the worst, quietest failure class.

## Who actually needs this

If any of these sentences describes you, you have a heartbeat-shaped blind spot:

- **"Our backups run nightly."** The classic. Backup scripts fail for mundane reasons — a full disk, an expired credential, a renamed bucket — and nobody reads backup logs until restore day. A heartbeat on the backup script turns "three weeks of missing backups" into "one morning alert."
- **"We have a worker that processes the queue."** A crashed consumer lets the queue grow silently for hours while the website looks perfectly healthy. A per-cycle heartbeat catches the crash in minutes.
- **"Billing runs on the 1st of every month."** Low-frequency jobs are the most dangerous: a monthly job that failed has *already* cost you a month by the time anyone wonders about it.
- **"The device is behind the customer's firewall."** You can't probe into NAT — but the device can always ping out. One heartbeat per unit tells you which box went dark and when.
- **"That server migration was months ago."** Migrations are where crontab entries go to die. A heartbeat per job is the only way to notice the ones that didn't survive the move.

## The anatomy of a good heartbeat monitor

The concept is simple, but the details decide whether it's trustworthy or noisy. Four things matter:

### 1. Schedules that match reality

Two modes cover everything: a **fixed interval** ("I should hear from this worker every 5 minutes") and a real **cron expression** ("03:00 daily, Europe/Vilnius") — timezone-aware, DST included. If your monitor can't speak cron, you end up approximating "every weekday at 3 AM" with a 24-hour interval and eating false alarms on the weekend.

### 2. A grace period

Jobs are never perfectly punctual — a backup that takes 10–40 minutes depending on data size is healthy, not broken. **Grace** is how much lateness you forgive before alerting: tight for frequent workers (a minute), generous for long-running jobs. Without it, you get paged for a job that's simply five minutes slow; with too much of it, you learn about failures late. Tune per job.

### 3. "Didn't run" and "ran but failed" are different problems

A ping's *presence* says the job ran. But what if it ran and **failed**? A good heartbeat setup reports the outcome too — the process **exit code** appended to the ping (`/0` for success, `/2` for a failure, `/fail` for an explicit failure signal). That splits your alerts into two distinct stories:

- **Missed schedule** → the job never started (dead cron, dead machine, dead container)
- **Run failed** → the schedule is alive, but the work itself is broken

Different problems, different fixes, different urgency. Collapsing them into one alert loses exactly the information you need at 3 AM.

### 4. Run duration and context

Send a ping at the job's **start** and another at the **finish**, sharing a run ID, and every run's duration gets measured — so "the export suddenly takes 40 minutes instead of 5" becomes an alert *before* it becomes an outage. And when the job attaches its last log lines to the ping, diagnosing a failed run starts in your monitoring dashboard, not with SSH.

## Setting it up is one line

The integration cost is deliberately near zero. For a cron job:

```bash
# crontab: define the ping URL once, report success or failure
PING=https://ingest.watchfor.io/p/<your-token>

0 3 * * * backup.sh && curl -fsS $PING/0 || curl -fsS $PING/fail
```

Any HTTP client works — curl from a shell script, a `fetch()` in a Node worker's loop, an HTTP call from a Kubernetes CronJob. The URL token is the only secret; there's no agent, no SDK, no firewall hole.

## Common pitfalls

- **Monitoring the machine instead of the job.** A server that's up says nothing about whether cron ran your script. The heartbeat has to come from the *job itself*, at the end of a successful cycle.
- **One heartbeat for many jobs.** If five jobs share a ping URL, four can die unnoticed while the fifth keeps checking in. One monitor per job — they're cheap.
- **Pinging before the work is done.** A ping at the *start* of the script tells you the script started, not that the backup succeeded. Ping on completion (or use start/finish pairs and get duration tracking for free).
- **Grace set by guesswork.** Watch a week of real run times, then set grace slightly above the natural variance. Revisit when the job's workload grows.

## Heartbeats in WatchFor

WatchFor's [heartbeat monitors](/heartbeat-monitoring) implement all of the above: interval and [real cron-expression schedules](/cron-job-monitoring) with timezones and grace, exit-code awareness (missed / failed / specific code alert separately), start–finish run duration tracking, and up to 10 KB of job output attached to every ping. A missed window opens an incident immediately — a daily backup that missed once has already lost a day — and the next successful check-in auto-resolves it.

A heartbeat counts as a regular monitor on every plan, including Free, and the ping URL conventions are Healthchecks-compatible (`/start`, `/fail`, `/<exit-code>`, `?rid=`), so migrating existing scripts means replacing the base of the URL. Details in the [docs](/docs/monitors/heartbeat).

## The bottom line

| | In one line |
| --- | --- |
| **What** | Alerting when an expected check-in *doesn't* arrive. |
| **For** | Cron jobs, backups, workers, pipelines, IoT — anything scheduled. |
| **vs uptime** | Uptime pings the service; heartbeat waits for the job to ping. |
| **Catches** | Silent failures — the job that just stops running. |
| **Cost of skipping it** | You find out when you need the backup. |

Uptime checks cover what's public. Heartbeats cover what's invisible. Most teams need both — and the invisible half is usually the one nobody is watching.

Related: [Uptime Monitoring 101](/blog/uptime-monitoring-101), [synthetic vs real user monitoring](/blog/synthetic-vs-real-user-monitoring).

---

Canonical page: https://watchfor.io/blog/heartbeat-cron-monitoring · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
