Healthchecks

Cron job and scheduled task monitoring

Monitoring & Status ★ 10.4k stars Easy setup BSD-3-Clause

Healthchecks is a self-hosted service that monitors cron jobs and scheduled tasks using dead man's switch pings. It targets administrators who need to know when a recurring job silently fails. It is deployed via Docker or a Django install.

Key features

  • Dead man's switch for cron jobs
  • Many notification integrations
  • Per-check schedules and grace
  • REST API

Pros & cons

Strengths

  • Catches silently failing jobs
  • Lots of notification channels
  • Lightweight

Trade-offs

  • Narrow use case
  • Not for metrics

Healthchecks replaces

Last reviewed Aug 26, 2026 · 872 words

Uptime Kuma tells you a service is down. Healthchecks tells you a job that was supposed to run did not, and that is the failure that actually loses data: the backup that has quietly not executed since March, the certificate renewal that stopped, the database dump whose disk filled. It works by expecting a ping on a schedule and alerting when the ping is late. It runs in 256 MB, it has been around since 2015, and the whole integration is one line appended to a crontab.

The entire integration is one curl

Each check gets a unique ping URL, a period (how often it should run) and a grace time (how late is too late). The job pings the URL when it finishes. If Healthchecks does not hear from it within period plus grace, it alerts.

0 3 * * * /usr/local/bin/backup.sh && curl -fsS -m 10 --retry 5 -o /dev/null https://hc.example.com/ping/8f4c1e2a-0b6d-4e3a-9c11-2f5d8a7b1c90

The && matters: the ping only fires if the script exits zero, so a failed backup is silent from the job's side and Healthchecks raises it as "late". The --retry 5 matters too; a monitoring ping that itself fails on one dropped connection is a false alarm generator.

That gets you 80% of the value. The wrapper below gets the rest. /start records when the job began, so you get run durations and a separate alert if a job starts and never finishes; appending the exit status to the URL reports a failure immediately instead of waiting out the grace period; and anything you POST as the body is stored as the log for that run.

URL=https://hc.example.com/ping/8f4c1e2a-0b6d-4e3a-9c11-2f5d8a7b1c90
curl -fsS -m 10 --retry 5 -o /dev/null "$URL/start"
OUT=$(/usr/local/bin/backup.sh 2>&1); RC=$?
curl -fsS -m 10 --retry 5 -o /dev/null --data-raw "$OUT" "$URL/$RC"

Schedules accept cron expressions with a timezone, so "every weekday at 02:30 Europe/London" is a first-class check rather than a fudge. That is the feature that separates it from the push monitor in Uptime Kuma, which can do a crude version of the same trick but has no cron grammar, no start signal and no per-run logs.

Setup is Docker, SQLite and one superuser

The official image is a Django app that defaults to SQLite, which is fine for a homelab with a few hundred checks. Postgres is an environment variable away if you outgrow it.

services:
  healthchecks:
    image: healthchecks/healthchecks:latest
    ports:
      - "8000:8000"
    environment:
      - SITE_ROOT=https://hc.example.com
      - SITE_NAME=Homelab checks
      - SECRET_KEY=replace-with-50-random-characters
      - ALLOWED_HOSTS=hc.example.com
      - REGISTRATION_OPEN=False
      - [email protected]
      - EMAIL_HOST=smtp.example.com
      - EMAIL_PORT=587
      - [email protected]
      - EMAIL_HOST_PASSWORD=change-me
      - EMAIL_USE_TLS=True
    volumes:
      - ./data:/data
    restart: unless-stopped

Create the first account with docker compose exec healthchecks /opt/healthchecks/manage.py createsuperuser, then keep REGISTRATION_OPEN=False. SITE_ROOT must be the public URL, because it is baked into every ping URL the interface hands you; get it wrong and every check you create points at the wrong host.

Do not run it on the machine it watches

This is the trap, and it is the one everyone walks into. Healthchecks installed on the NAS, monitoring the NAS backups, tells you nothing when the NAS is off. It needs to live somewhere the monitored jobs are not: a second box, a $5 VPS, a Raspberry Pi in a different room on a different power strip. And even then, something must watch the watcher. The tidy answer is the hosted healthchecks.io, whose free tier covered 20 checks at last check: create one check there, have your self-hosted instance's own cron ping it hourly, and you have a two-layer dead man's switch for the price of zero.

Notifications: ntfy for the phone, email for the record

The integrations list is long: email, Slack, Discord, Telegram, Matrix, Signal, Pushover, PagerDuty, Opsgenie, generic webhooks, and the two self-hosted push servers, ntfy and Gotify. My split is ntfy for anything that should wake me and email for everything, because the email is the searchable history six months later when you wonder how often the offsite job actually failed. Alerts fire once when a check goes down and once when it recovers; there is no repeating nag by default, so pair it with a channel you actually read.

What I'd do

One Healthchecks container on the VPS that runs nothing else important, SQLite, ntfy and email wired in, registration closed. A check for every restic or Borg job, every certificate renewal, every scheduled database dump, and every "I'll remember to run this monthly" script, with the start-and-exit-status wrapper on the ones that matter. One check on healthchecks.io watching the instance itself. It sits alongside Uptime Kuma rather than replacing it; together they cover both halves of the monitoring question, whether things are up and whether things ran. For the cost of 256 MB and an afternoon, it is the highest-value monitor in the whole stack.

Compare Healthchecks

13 head-to-head comparisons.

Similar monitoring & status apps