Here's a failure mode regular uptime monitoring 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:
# 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 implement all of the above: interval and real cron-expression schedules 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 — migrating existing scripts is usually just swapping the domain. Details in the docs.
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, synthetic vs real user monitoring.