Uptime Kuma is a good monitor. It is also a single Node process, on a single host, usually on the same network as half the things it watches, and it has one failure mode it can never report: itself. When the container stops after a host reboot, when kuma.db fills the disk and SQLite starts refusing writes, when the home connection drops, or when an upgrade leaves the web UI serving but the check loop stalled, you get no alert. You also get no alert for anything else that breaks during that time. The dashboard, if you can reach it, shows the last state it knew about, which was green.

This is the "who monitors the monitor" problem, and the only honest answer is: something outside Kuma's failure domain. This guide builds that watchdog with CronAlert, from the outside in: is the Kuma UI serving, is its status page serving, is the check loop actually running, did the backup run, and are the alerts routed somewhere that is still up when the Kuma host is not. Everything except the heartbeats runs on the free plan.

What a dead Uptime Kuma looks like

Before the monitors, a short inventory of how Kuma goes quiet. Every one of these ends in silence rather than an alert, which is the whole point.

FailureUIChecksAlerts
Container stopped (reboot without restart policy, image update gone wrong, OOM kill)DownStoppedNone
Host offline or lost its uplinkDownRunning, every monitor "down"Cannot leave the host
Disk full, SQLite cannot writeUp, errors in the logResults not savedOften fail too
Check loop stalled after an upgrade or a bad monitorUp, last beats frozenStoppedNone
Notification provider broken (expired bot token, dead ntfy server)UpRunningSent and lost
Reverse proxy or tunnel in front of it brokenUnreachableRunningWorking, if egress is fine

Two of these, the stalled loop and the broken provider, leave the UI green and reachable. A plain HTTP check on the hostname catches the first three and misses those two, which is why the heartbeat in layer 3 is the one that matters most.

Layer 1: the UI is serving

Uptime Kuma serves its single-page app from the root URL and answers 200 whether or not you are signed in; the login form is rendered client-side. That makes the root the right liveness target, with a keyword to prove the HTML is Kuma's and not a proxy's error page:

URL:              https://kuma.example.com/
Method:           GET
Expected status:  200
Keyword (Pro):    contains "Uptime Kuma"

If Kuma sits behind a reverse proxy, monitor the proxy separately so an alert tells you which of the two broke. If it is only on your LAN, you have two options: publish it through a Cloudflare Tunnel, optionally behind Access with a service token in the monitor's custom headers, or skip the inbound checks entirely and rely on the outbound heartbeat in layer 3, which works from any network with egress. A Kuma on a bare private IP like 192.168.1.20:3001 cannot be reached by any public monitor, CronAlert included.

Kuma also exposes Prometheus metrics at /metrics. With API keys enabled under Settings → Security, it takes HTTP Basic auth with any username and the key as the password:

URL:              https://kuma.example.com/metrics
Expected status:  200
Custom headers:   { "Authorization": "Basic <base64 of ":uk2_your-api-key">" }
Keyword (Pro):    contains "monitor_status"

This proves the app is up and the monitor table loaded, which is slightly more than the root check proves. It does not prove the checks are fresh, since the gauges exist as soon as monitors are registered. The Kuma wiki labels this an internal API that can change without notice, so treat it as a bonus rather than the foundation.

Layer 2: the status page is serving

If you publish a Kuma status page, it has two public URLs worth a monitor. The page itself at /status/<slug> is what your users see, and the data behind it is served as JSON from an unauthenticated endpoint while the page is published:

URL:              https://kuma.example.com/api/status-page/heartbeat/<slug>
Expected status:  200
Keyword (Pro):    contains "heartbeatList"

A 404 here means the slug is wrong or the page was unpublished, and a 200 without heartbeatList means something other than Kuma answered. The response holds up to 100 recent beats per monitor, with status codes 0 (down), 1 (up), 2 (pending), and 3 (maintenance). It is tempting to add not_contains "status":0 to turn CronAlert into a second alert path for Kuma's own findings. It works, with a caveat: a down beat stays in that 100-beat window for a long time (about 100 minutes at a 60-second interval), so the CronAlert monitor stays red well after Kuma reports recovery. Use it if you want the second path and understand the lag; otherwise keep this monitor about serving, not about results.

Layer 3: Kuma proves it is checking (the heartbeat)

This is the layer that catches the failures a UI check cannot see. Instead of asking Kuma whether it is running, make Kuma tell you, on schedule, from inside its own check loop. Create a heartbeat monitor in CronAlert, copy its ping URL, and then in Uptime Kuma create an ordinary HTTP(s) monitor whose URL is that ping URL:

Uptime Kuma monitor
  Type:               HTTP(s)
  Friendly name:      Watchdog → CronAlert
  URL:                https://cronalert.com/api/heartbeat/<token>
  Heartbeat interval: 60 seconds
  Retries:            0

CronAlert heartbeat monitor
  Expected interval:  1 minute
  Grace period:       5 minutes

CronAlert's heartbeat endpoint accepts GET and POST and answers 200 with a small JSON body, which Kuma's default accepted-status range treats as up, so the Kuma side stays green forever and is not something you need to look at. The CronAlert side is the signal. Every minute that Kuma runs the check, the heartbeat is pinged. The ping arrives only if the Kuma process is alive, its scheduler fired, the monitor actually executed, and the host has a working route to the internet. Break any link and the heartbeat goes overdue, and CronAlert alerts you from Cloudflare's network, not from the box that just went dark.

Set the grace to a few minutes rather than zero. Kuma's intervals drift slightly under load, a container restart takes thirty seconds, and you want an alert that means "Kuma has been silent for five minutes", not "one beat was late". The grace-period guide covers the trade-off. If you would rather not spend a 1-minute heartbeat on it, set Kuma's interval to 300 seconds and the CronAlert interval to 5 minutes with a 5-minute grace; you learn about a dead Kuma within ten minutes instead of six. This is the same pattern as the Prometheus Watchdog alert into a heartbeat: the monitoring system emits a steady signal, and the absence of the signal is the alert.

A note on what this does not prove. It proves the loop runs and the host has egress; it does not prove every other monitor in Kuma is configured correctly or that Kuma can reach your internal services. For that, you still read Kuma. What you no longer have to wonder is whether Kuma is reading anything.

Layer 4: the database backup ran

Kuma's entire state, every monitor, notification, status page, and the history behind the graphs, is one SQLite file at data/kuma.db (2.0 can also use MariaDB, in which case back that up instead). Copying the file while Kuma is writing to it can produce a corrupt copy, so use SQLite's own backup command, then ping a heartbeat only if it succeeded:

0 3 * * * sqlite3 /opt/uptime-kuma/data/kuma.db ".backup /backups/kuma-$(date +\%F).db" \
  && curl -fsS --retry 3 https://cronalert.com/api/heartbeat/<backup-token>

Inside Docker, run the same via docker exec against the container, or mount the data volume where the host's sqlite3 can reach it. Set the heartbeat to a 24-hour interval with a 2-hour grace. The && is the important character: a failed backup must not report as a successful one. Monitoring scheduled backups goes further on verifying the artifact, not only the exit code, and the same cron can back up the data/ directory alongside, which holds uploaded status-page logos.

Layer 5: the alert path does not die with the host

A watchdog that alerts into the thing it is watching is decoration. Check two things about your alert routing.

First, where CronAlert alerts go. Email and mobile push arrive regardless of what happened to your infrastructure. A webhook into a self-hosted relay, or a Slack workspace you only read at a desk, is weaker for this specific monitor. Push on Android and iOS are the right channels for "your monitor is dead".

Second, where Kuma's alerts go, since they are the reason you run it. A common homelab layout puts ntfy or Gotify in a container next to Kuma. When the host dies, both die, and nothing Kuma noticed in its last minutes got through. Either host the notification server elsewhere, or add a plain CronAlert HTTP monitor on it so you learn separately when it goes away. The same applies to a Telegram bot whose token was reset or a Discord webhook that was deleted: Kuma believes it sent the alert. Test the delivery path on a schedule rather than trusting it; testing your alert channels describes how.

Layer 6: the host underneath

Kuma lives on something, and that something fails for its own reasons: a VPS whose disk filled, a Raspberry Pi whose SD card wore out, a Proxmox node that rebooted into a kernel that did not bring up the bridge. A monitor on the host, or on another service the host runs, separates "Kuma is broken" from "everything on that machine is gone". Monitoring the server, Docker and self-hosted apps, and Proxmox each cover their layer. If the Kuma host is a NAS, the NAS guide adds the disk and share checks.

Reading the alerts

AlertWhat brokeFirst thing to check
Watchdog heartbeat overdue, UI monitor downKuma process, container, host, or uplinkdocker ps on the host; can you reach anything else there?
Watchdog heartbeat overdue, UI monitor greenCheck loop stalled, or the host lost egress but not ingressKuma's log; whether other monitors' last beats are advancing
UI down, watchdog heartbeat fineReverse proxy, tunnel, or DNS in front of KumaProxy logs, tunnel connector status
Status-page endpoint 404Page unpublished or slug changedStatus page settings in Kuma
Backup heartbeat overdueBackup cron did not run or .backup failedCron log, free space on the backup target
Notification-server monitor downKuma's alerts are going nowhereThat container; assume Kuma alerts since then were lost

Alert channels in CronAlert are team-wide. If you want the watchdog alerts on push and everything else on Slack, put the watchdog monitors in their own team; alert routing with teams explains the pattern.

Frequently asked questions

Isn't this just replacing Uptime Kuma with CronAlert?

No. Kuma keeps doing what it does well: checking internal services on your LAN, hundreds of them, with whatever notification providers you like, for free. CronAlert watches the two or three things Kuma cannot: itself, its backup, and its alert path. If you would rather consolidate, the full comparison is honest about where each wins. Most people who do this keep both.

Can I point CronAlert's alerts into a Kuma push monitor, so Kuma shows CronAlert's status too?

Not cleanly. Kuma's push monitors are marked up or down by a request to /api/push/<token>?status=up|down. CronAlert's webhook alert channel POSTs the same URL for both the incident and the recovery, so you cannot select status per event without a small relay in between. Webhook alert integrations covers writing one if you want the mirror.

Does the watchdog check count against CronAlert's monitor limits?

Yes, as one heartbeat monitor, plus one HTTP monitor each for the UI and status page if you add them. The free plan has 25 monitors; a Kuma watchdog uses two or three of them.

Uptime Kuma 2.0 changed things. Does this still apply?

Yes. The root URL, status-page API, push endpoint, and HTTP monitor type are unchanged. The main 2.0 difference for this guide is the optional MariaDB backend, which moves the backup from sqlite3 .backup to mariadb-dump, with the same heartbeat at the end.

Which plan do I need?

The HTTP checks on the UI, metrics, and status page are on every plan, at 3-minute intervals on Free and 1-minute on Pro. The watchdog heartbeat, the backup heartbeat, and keyword assertions are Pro ($5/mo, or $4/mo billed annually). If you want to stay on Free, the UI check alone catches the stopped-container and dead-host cases, which are the common ones.

Monitor the thing that cannot report itself

Everything Uptime Kuma tells you depends on Uptime Kuma running, and that is the one fact it cannot deliver. One HTTP check on the UI, one Kuma monitor pointed at a CronAlert heartbeat, and one heartbeat on the backup close the gap for a couple of dollars a month, or for nothing if the UI check is enough for you. Create a free account, add the UI monitor, and add the watchdog heartbeat before the next host reboot you did not notice. Related reading: heartbeat monitoring for cron jobs, monitoring Home Assistant and Pi-hole for the rest of the homelab, keeping false positives down, and choosing between HTTP, TCP, and heartbeat monitors.