Self-Host Healthchecks: Know the Instant a Cron Job Dies

Self-Host Healthchecks: Know the Instant a Cron Job Dies

Here is a fun way to lose a weekend. Your nightly database backup cron job has been failing silently for three weeks. The command errored out, cron swallowed the message, and nobody noticed, because a job that does not run does not send you anything. Then a disk dies, you reach for the backups, and the newest one is from a month ago. I have been on the wrong side of this exact situation, and it is a special kind of stomach drop.

The problem is structural: most monitoring watches for something bad happening. But a cron job that stops running is the absence of something good. There is no error to catch, no alert to fire. You need a tool that gets suspicious when it stops hearing from you. That is a dead man's switch, and Healthchecks is the self-hosted one worth running.

How it works

The model is refreshingly simple. You create a "check" in Healthchecks and it hands you a unique ping URL. Your job hits that URL every time it finishes successfully. Healthchecks knows the schedule you expect, so if a ping does not arrive on time, it flips the check to "down" and alerts you.

That inverts the usual logic. You are not watching for failures. You are watching for silence. If your backup runs, Healthchecks stays quiet. If it does not, that silence is the alert.

Each check understands three signals:

  • Success: hit the base ping URL when the job finishes cleanly.
  • Start: append /start before the job runs, so Healthchecks can measure how long it took.
  • Failure: append /fail, or send the exit code directly, to report that the job ran but broke.

Why self-host it

The hosted service is fine, but there is a real irony in trusting a third party to tell you when your infrastructure goes quiet. If your monitoring and your servers share a dependency, or you would rather not route the operational heartbeat of your whole stack through someone else's SaaS, you self-host.

There is also the data angle. Your check names alone leak a lot: "prod-db-nightly-backup", "customer-export-hourly", "stripe-reconcile". That is a map of how your business runs. Keeping it on your own box is just tidier.

Deploy it on Elestio

Healthchecks is a Django app backed by Postgres, and it needs an SMTP sender so it can actually email you when something goes down. On Elestio it is a one-click deploy that wires up the database, TLS, and backups for you, starting at around $11/month for the VM. Spin one up from the Healthchecks on Elestio page.

Once it is up, log in, create your first check, set the expected period (say, every 1 day) and a grace time (say, 1 hour of slack before it panics), and copy the ping URL.

Wiring up your jobs

The simplest integration is one line at the end of your cron job. Ping on success:

# Runs your backup, then pings Healthchecks only if it succeeded
0 2 * * * /usr/local/bin/db-backup.sh && curl -fsS -m 10 --retry 5 https://your-hc.vm.elestio.app/ping/<uuid>

The -m 10 caps the request at 10 seconds and --retry 5 handles a flaky network, so the ping itself never becomes the thing that fails.

That tells you if the job stopped running. To also catch a job that runs but errors out, wrap it and report the exit code:

#!/bin/bash
URL="https://your-hc.vm.elestio.app/ping/<uuid>"

# Signal that the job started (lets Healthchecks track duration)
curl -fsS -m 10 --retry 5 "$URL/start"

/usr/local/bin/db-backup.sh
EXIT=$?

# Send the actual exit code: 0 pings success, anything else pings failure
curl -fsS -m 10 --retry 5 "$URL/$EXIT"

Now you get three distinct states: the job ran and passed, the job ran and failed, or the job never checked in at all. You can even pipe a few lines of log output into the ping body so the alert shows you why it broke, not just that it did.

Schedules and grace time

Two settings decide when a check is considered late, and getting them right is the whole game.

Setting What it does
Period (simple) How often you expect a ping. Miss it and the check goes late.
Cron expression Match your real crontab exactly, with a timezone, for jobs on odd schedules.
Grace time How long a job is allowed to be late before it is marked down and alerts fire.

For a job that normally takes 40 minutes, do not set a 2-minute grace time. You will get paged every time it runs a little slow. Match the grace to reality.

When a check does go down, Healthchecks fans the alert out to whatever you have connected: email, Slack, Discord, Telegram, PagerDuty, webhooks, and plenty more.

Troubleshooting

  • Alerts never arrive: almost always SMTP. Confirm the outgoing mail settings and send a test. No working SMTP means no email alerts, full stop.
  • Constant false alarms: your grace time is shorter than the job's real runtime, or the check's schedule does not match the crontab. Widen the grace or switch the check to a cron expression.
  • Pings not registering: if your job runs somewhere locked down, the box may not be allowed to make outbound HTTPS requests. Check egress rules to your Healthchecks host.
  • Timezone mix-ups on cron checks: a cron-scheduled check defaults to UTC. Set the check's timezone to match the server, or your "late" moment will be off by hours.

Turn silence into a signal

A backup you never test is a wish, and a cron job you never monitor is a guess. Healthchecks turns both into something you can actually see: green when it ran, loud when it didn't. It takes about ten minutes to wire up your most important job, and it is the kind of thing you are grateful for exactly once, on the worst possible day.

Spin one up on the Healthchecks on Elestio page and point your scariest cron job at it first.

Thanks for reading ❤️ See you in the next one 👋