Cron has a cruel design flaw: it tells you nothing when a job stops running.
A job that exits non-zero? Silent. A job that never fires because the host was
asleep? Silent. A backup script that's been dying at 2 a.m. for three days
straight? Completely silent — right up until the morning you actually need that
backup and discover the last good copy is from Tuesday.
The failure mode isn't "the job errored." It's the absence of success, and
absence is exactly the thing a logs-and-exit-codes mental model can't catch.
The usual fix, and why it bugged me
The classic answer is a heartbeat monitor: your job pings a URL when it finishes,
and a watchdog yells if the ping doesn't arrive on time. healthchecks.io,
Cronitor, Dead Man's Snitch — all good, all battle-tested.
But for a box of personal cron jobs and a few self-hosted boxes, they felt like
overkill:
- I had to create an account.
- My job names and timing metadata went to a third party.
- It was one more SaaS dashboard to log into and forget about.
I wanted the heartbeat idea without the hosted part. So I built deadcron —
a single CLI, zero dependencies, no server, no signup. State lives in a JSON file
under ~/.deadcron. Nothing leaves your machine unless you explicitly point it at
a webhook or SMTP server.
How it works
Three moving parts:
- Each run checks in. Wrap your command and deadcron records a "ping" on exit 0 (and the exit code on failure):
# before:
0 2 * * * /opt/backup.sh
# after:
0 2 * * * deadcron run backup --every 1d --grace 1h -- /opt/backup.sh
A job declares its expected rhythm with
--every(plus an optional
--grace). If the time since the last successful check-in exceeds
every + grace, the job is overdue.A watchdog runs
checkon its own schedule. You add it to cron once:
deadcron install --every 5m
That's it. Now if backup doesn't succeed within a day (+1h slack), the next
check tick notices the silence and alerts you.
$ deadcron status
● backup ok every 1d last: 3h ago
● sync OVERDUE by 2h every 1h last: 5h ago
● report FAILED (exit 1) every 1w last: 2d ago
check exits non-zero when anything is overdue or failed, so it also drops
straight into CI or any other scheduler:
deadcron check # exit 1 if unhealthy
deadcron check --json # machine-readable
Alerts, but only where you want them
check fires every channel you've turned on — and they all run locally:
deadcron config enable macos # native notification
deadcron config set-webhook https://hooks.slack.com/services/XXX
deadcron config set-email --to me@you.com --sendmail
deadcron config test # send a sample through all of them
| Channel | How |
|---|---|
| terminal | writes to stderr (great for CI / piping) |
| macOS | native banner via osascript
|
| webhook |
POST JSON {event, checkedAt, jobs} → Slack/Discord/your endpoint |
local sendmail, or direct SMTP (TLS 465 / STARTTLS, optional auth) |
Repeat alerts are throttled (default: once per hour per job) so a long outage
doesn't turn into a notification flood.
Install
It ships on both registries, because half my cron jobs are Node and half are
Python:
npx deadcron # Node — zero deps
pip install deadcron # Python — pure stdlib
Both implementations share the exact same ~/.deadcron state format, so you can
mix them on one machine and they won't step on each other.
A few design notes
-
No daemon. deadcron isn't a long-running process — it's just a CLI plus a
state file. The "watchdog" is literally
deadcron checkinvoked by your own cron. Less to crash, less to babysit. -
runoverping. You can calldeadcron ping <name>at the end of a script, but wrapping withrunalso captures non-zero exit codes, so a job that fires on time but crashes is still flagged. -
gracematters. A job that runs "every hour" never runs at exactly one hour. Grace is the slack that keeps borderline timing from crying wolf.
Try it / break it
Code, issues, and the full README:
It's MIT, it's tiny, and I'd genuinely like to know where it falls over. If
you've got a cron job you'd be sad to lose silently, point deadcron at it and
tell me what's missing.
What do you use today to know your scheduled jobs are actually running?
Top comments (0)