Skip to content

Guide

How to know when a cron job or backup stops running

A job that fails loudly gets noticed. A job that stops running doesn't: no error, no email, nothing in the logs, until the day you need last night's backup. The usual answer is a heartbeat.

Why a stopped job is silent

Cron runs what's in the crontab, on a machine that's up. If the server was replaced, the crontab edited, the disk filled or the script stuck waiting for a lock, nothing runs and nothing complains. Cron's own email (MAILTO) only tells you about a job that ran and printed something.

A heartbeat: an alert on silence

A heartbeat turns it around. The job pings an address each time it finishes, and something else expects that ping and alerts you when it doesn't come on time. It's also called a dead man's switch. It catches every way a job can stop, because it doesn't need to know why.

You can build the other end yourself: an address that notes the time of the last ping, and a check on another machine that alerts when that time is too old. Then that check needs watching too, which is why most people use a service for it.

The one line

Put the ping after the job, joined with &&, so it's sent only when the job succeeds:

0 3 * * * /usr/local/bin/backup.sh && curl -fsS --retry 3 https://hb.badgerbell.com/<your id> > /dev/null
Put your heartbeat's address in place of the one shown.
  • -f makes curl fail on an error answer, -sS keeps it quiet unless something goes wrong, and --retry 3 tries again after a network blip.
  • The job's exit code decides. A backup script that hides its own errors and exits with 0 still pings, so make it exit with an error when it fails.

To hear about a failure at once, rather than when the ping is late, ping /fail when the job fails. The message after m= goes into the alert:

if /usr/local/bin/backup.sh; then
  curl -fsS --retry 3 https://hb.badgerbell.com/<your id> > /dev/null
else
  curl -fsS --retry 3 "https://hb.badgerbell.com/<your id>/fail?m=backup+failed" > /dev/null
fi
Ping /start when a run begins, too, to time it: with a run-time limit, a run that hasn't finished within it counts as failed.

The grace period

The grace period is how late a ping can be and still count as on time. Make it longer than the job's slowest run, plus any wait before it starts. A nightly backup that usually takes 10 minutes and sometimes 40 needs more than 40 minutes' grace, or it alerts on its slow nights.

Time zones

BadgerBell's heartbeat doesn't read your crontab. It expects a ping once every period, counted from the last one: "every 1 day", "every 1 hour". So the server's time zone doesn't matter. One thing does: when the server's clock follows daylight saving, a job at 03:00 runs 23 or 25 hours apart twice a year. A grace period of more than an hour covers it.

With BadgerBell

  • Each heartbeat has its own address at hb.badgerbell.com, with /fail and /start beside it. Pings are GET requests, as curl sends them. A HEAD request (curl -I) doesn't count.
  • Set the period and the grace, or start from Nightly (every day, 30 minutes' grace), Hourly (10 minutes) or Weekly (1 hour).
  • No ping within the period and the grace, or a ping to /fail, and the heartbeat goes down and you're alerted. The next good ping brings it back up.
  • A heartbeat counts as 1 monitor, however often the job runs. It's on every plan, Free included.

Set it up with your assistant

Connect BadgerBell, paste one prompt, and your assistant proposes your monitors.

1. Connect BadgerBell
claude mcp add --transport http badgerbell https://app.badgerbell.com/mcp
Your assistant asks you to sign in to BadgerBell. You see every connected agent, and can revoke it, in the app.
2. Paste this prompt
Find every scheduled job in this project: cron, systemd timers, CI schedules, backups. For each, show me the line to add that pings a BadgerBell heartbeat on success, and the schedule and grace period to set. Add the heartbeats in BadgerBell only after I say yes.

Anything public or destructive waits for you in BadgerBell.