undownable

Cron job monitoring with a dead man's switch

Updated Aug 19, 2026

A cron job that fails loudly is a good day. The dangerous one is the job that stops running: a crontab wiped by a server rebuild, a timer never re-enabled after a reboot, a backup container in a crash loop since Tuesday. Nothing errors, because nothing runs.

Silence is the failure mode

Every other check type works by us reaching out to you. Heartbeat monitoring inverts that: your job calls us when it finishes successfully, and the absence of that call is the signal. The name comes from railway safety: the system is safe only while something keeps actively confirming it.

It suits anything scheduled and invisible: nightly database dumps, restic or Borg runs, certificate renewal hooks, data imports, log shippers. Also self-hosted monitoring stacks. An Uptime Kuma instance cannot notify you that it has stopped, but it can hold open a heartbeat that goes quiet when it dies.

Your private ping URL

A heartbeat monitor gets a ping URL of the form https://undownable.com/ping/{id}, where the id is an unguessable ULID. That URL is the whole interface: it accepts GET or POST, needs no authentication header, and returns a small JSON body. The ULID is the shared secret, which makes it usable from a bare curl line in a crontab with no credential file.

Heartbeat monitors have no target field, because there is nothing for us to reach. They have an expected ping interval and a grace period instead: the interval is how often you promise to call, and the grace period, 60 seconds by default, is the slack on top. If a nightly backup sometimes runs 20 minutes long, cover that variance there.

The state machine is deliberately plain. A monitor becomes due one interval after its last ping, so being evaluated at all means the ping is already late. Miss the grace period too and the monitor reports down with a message naming how long it has been silent, an incident opens, and your channels are alerted. A monitor never pinged reports that no heartbeat has arrived, so a job never wired up cannot masquerade as healthy.

Ping on success, not on start

The one detail that matters most: put the ping after the work, chained on success. Ping at the top of the script and you are monitoring whether cron fired, not whether the job worked. Chaining with && means the ping only happens when the real command exited zero, so a backup that runs every night and fails every night goes quiet and pages you.

A crontab line. The curl flags earn their place: -f fails on HTTP errors, -s and -S stay quiet unless something goes wrong, and -m stops a network hiccup wedging the job.

0 2 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 https://undownable.com/ping/01J9XKQ2M8Z3W7B4YT6N0V5D2C

For a systemd timer, the ping is a second ExecStart in the service unit. systemd runs them in order and abandons the rest if an earlier one fails, giving the same success semantics as the && above.

# /etc/systemd/system/backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
ExecStart=/usr/bin/curl -fsS -m 10 https://undownable.com/ping/01J9XKQ2M8Z3W7B4YT6N0V5D2C

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target

In a scheduled GitHub Actions workflow, add a final step. Without an if: condition it runs only when every earlier step succeeded, which is what you want. Keep the id in a repository secret if the repo is public.

on:
  schedule:
    - cron: '0 2 * * *'

jobs:
  nightly:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./bin/nightly
      - name: Report the heartbeat
        run: curl -fsS -m 10 https://undownable.com/ping/${{ secrets.HEARTBEAT_ID }}

Windows Task Scheduler has no equivalent of && across actions, so wrap both halves in one PowerShell command and gate the ping on the exit code.

powershell -NoProfile -Command "& 'C:\Scripts\backup.ps1'; if ($LASTEXITCODE -eq 0) { Invoke-RestMethod -Uri 'https://undownable.com/ping/01J9XKQ2M8Z3W7B4YT6N0V5D2C' -TimeoutSec 10 }"

Setting one up

Heartbeat monitors count as ordinary monitors, so ten fit on the free plan permanently, with 30 days of history and a status page. Alerts reach email, ntfy, Telegram, Slack, Discord, and generic webhooks on every plan, and delivery is exactly-once. Pro is a flat price per team, no per-monitor or per-seat charge: 50 monitors, one-minute intervals, a year of history, unlimited status pages and members, custom domains, after a 14-day trial with no card.

Questions people ask

Does the ping URL need an auth token?

No, and GET or POST both work. The monitor id is an unguessable ULID and serves as the secret, which lets a plain curl line work with no credential file. Treat the URL as a secret and use a CI secret when the pipeline config is public.

What if my job runs late but does finish?

That is what the grace period is for. A ping is only missed once the interval plus the grace period have passed in silence, and a late ping arriving after an incident opened resolves it automatically.

Can I use this to monitor my self-hosted monitoring?

Yes, and it is one of the best uses for it. A self-hosted Uptime Kuma or Alertmanager cannot report its own death. Point a push notification from that host at a heartbeat URL and Undownable becomes the watcher of the watcher.

What if the job pings twice in quick succession?

Nothing bad. Pings serialize through a row lock, so a burst cannot open duplicate incidents.

Related reading

Monitoring that watches from the outside

Free plan with 10 monitors, plus a 14-day Pro trial. No credit card required.

Start free