The scariest failure mode a scheduled job has is silence. The nightly backup didn't crash loudly β the server it ran on got rebuilt, the crontab entry didn't come along, and nothing anywhere is even trying to run it. An uptime monitor can't catch this: your site is up, your API is up; it's the job that's dead, and jobs don't have URLs to check.
The fix is a dead man's switch (a "heartbeat monitor"): instead of a monitor calling you, your job pings a URL every time it runs. If the pings stop arriving, the alarm fires. It's the model healthchecks.io built a whole product on, and it works for anything on a schedule β cron jobs, systemd timers, GitHub Actions crons, scheduled agent tasks, backup scripts, data pipelines.
If you want a cron-monitoring dashboard, use healthchecks.io. Their free "Hobbyist" plan gives you 20 checks with 100 log entries each (verified on their pricing page, September 2026) β a genuinely generous free tier, not a teaser. The project is open source and self-hostable, they give the $20/mo Business plan free to open-source projects and nonprofits, alerting covers email and dozens of integrations, schedules can be full cron expressions with time zones, and detection granularity goes down to a minute. It is the deserved default in this category.
What this page is for is the other case: you have one script and you want the switch armed right now, from the terminal you're already in. healthchecks.io needs a signup first (email + magic link) before you have a ping URL. That's thirty seconds for a human with a mail client β and a wall for a shell script, a CI step, or an agent that wants to arm a watchdog for itself.
curl -X POST https://mockbird.mockbird.workers.dev/api/status/heartbeat \
-H 'content-type: application/json' \
-d '{"name":"nightly backup","period_minutes":1440,"notify":"https://hooks.slack.com/services/T000/B000/XXXX"}'
period_minutes is how often the job is supposed to run; notify is a Slack incoming webhook, a Discord webhook, or any HTTPS endpoint you control. The response comes back immediately with everything you need:
{
"id": "hb-β¦",
"secret": "β¦",
"ping_url": "https://mockbird.mockbird.workers.dev/ping/p-β¦",
"ping": "curl -fsS -m 10 https://mockbird.mockbird.workers.dev/ping/p-β¦",
"period_minutes": 1440,
"grace_minutes": 720,
"alert_when": "no ping for longer than 24h + 12h grace (evaluated every 30 min)",
"status_page": "https://mockbird.mockbird.workers.dev/status/hb-β¦",
"badge_markdown": "[](β¦)",
"feed_atom": "https://mockbird.mockbird.workers.dev/status/hb-β¦/feed.xml"
}
No account, no email, no dashboard. A confirmation delivery hits your webhook right away so you know the channel works, and creation counts as the first ping β the clock is already running.
No webhook handy? Omit notify and you get a pollable heartbeat instead: the create response includes a poll URL that returns any missed-check-in / checked-in-again transitions since your last poll plus the current ping age — no endpoint to host. How poll mode works.
Append the ping to the end of the job with &&, so it only fires when the job succeeded:
# crontab: run backup at 03:00, check in only on success
0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://mockbird.mockbird.workers.dev/ping/p-XXXX
That one line covers all three failure modes: the job crashed (no && ping), the job never started (no ping), the whole machine is gone (definitely no ping). -fsS -m 10 keeps the ping quiet, failure-visible, and bounded. The same line works in a systemd timer's ExecStartPost, at the end of a GitHub Actions scheduled workflow, or as the last step of an agent's scheduled task.
We evaluate every 30 minutes. If no ping has arrived within period_minutes + grace (grace defaults to half the period, clamped 5mβ24h; override with grace_minutes), your webhook gets one service.down event β "missed its check-in" β and when pings resume, one service.up. No repeat nagging, no flood. Delivery formats are sniffed from the URL: hooks.slack.com gets {"text": β¦}, Discord webhooks get {"content": β¦}, and anything else gets a JSON payload signed with your secret (x-mockbird-signature: sha256=hex(hmac-sha256(secret, body))) β the signature-verification snippet in the uptime guide applies verbatim.
Every heartbeat also gets a public, unguessable-URL status page (/status/hb-β¦), a live README badge (/status/hb-β¦/badge.svg), and an Atom feed of missed/resumed check-in events (/status/hb-β¦/feed.xml) that any feed reader can watch. One deliberate design choice worth knowing: the ping URL is a separate secret from the page URL. You can embed the badge in a public README and nobody who sees it can fake a check-in β the page and badge show only the name and state, never the ping URL. Management (info, delete) requires the secret from the create response.
Facts checked September 2026 against each vendor's own pricing/FAQ pages β verify current numbers yourself:
| Free tier | Pick it when | |
|---|---|---|
| healthchecks.io | 20 checks, 100 log entries each, email + many integrations, cron-expression schedules, ~1-min granularity; open source, self-hostable; Business plan free for OSS/nonprofits | You're monitoring a fleet of cron jobs and want a dashboard, log history, and email β the deserved default. |
| Cronitor | 5 monitors free; usage-based paid ($2/mo per monitor) | You want cron monitoring plus performance metrics and status pages in one commercial tool (our Cronitor page). |
| Dead Man's Snitch | 1 free snitch, account required; integrations from $19/mo | You have exactly one job and like their smart-alert tooling (our page on it). |
| Mockbird heartbeat | 5 live heartbeats/IP (deleting frees the slot), 100 total, periods 30 minβ7 days, webhook alerts + public status page/badge/feed | You want the switch armed this minute with one curl, no account β or a script/CI job/agent is arming it for itself. |
Where the incumbents win, plainly: minute-level detection (our evaluation runs every 30 minutes, so period_minutes starts at 30 β this is for jobs on human schedules, not sub-minute liveness), email/SMS/push channels, cron-expression schedules with time zones, log history you can browse, teams, and years of production trust. We do: one curl, one webhook on miss, one on recovery, a status page + badge + feed, zero setup friction, no account anywhere in the loop.
# info + ping URL + recent evaluations
curl "https://mockbird.mockbird.workers.dev/api/status/heartbeat/hb-XXXX?secret=YOUR_SECRET"
# disarm
curl -X DELETE "https://mockbird.mockbird.workers.dev/api/status/heartbeat/hb-XXXX?secret=YOUR_SECRET"
The endpoint is self-documenting β GET /api/status/heartbeat returns full usage as JSON. If the URL you want to watch is a website rather than a job, the same machinery does classic URL monitoring: see the uptime-monitoring guide.
period_minutes 30β10080, grace defaults to period/2 (5mβ24h), 5 live heartbeats per IP (deleting one frees the slot immediately), 100 total (small free capacity β if you hit the cap, try later), evaluation every 30 minutes, best-effort webhook delivery, 5 consecutive failed deliveries auto-remove the heartbeat.