Gatus vs Uptime Kuma: Status Pages Compared
A YAML file and a Go binary against a web UI and a SQLite database

Contents
These two tools do the same job and disagree completely about how a person should tell a computer what to watch.
Uptime Kuma gives you a web UI, and you click through a form for each monitor. Gatus gives you a YAML file, and you write the whole system into it. Everything downstream — how you back it up, how you review a change, how you deploy fifty checks, how much RAM it eats — follows from that one decision.
I have run both for long enough to have a preference and to know why the other camp exists. Here is the comparison without the tribalism.
The configuration divide
This is the whole post, really. Everything else is a detail.
Gatus is a single YAML file, and here is a real one:
| |
Read that and you know exactly what is being watched, how often, and what counts as healthy. Put it in a repository and every change to your monitoring is a reviewable diff with a commit message. Point Flux at it, or just have a CI job restart the container on push, and your monitoring is genuinely declarative.
The Uptime Kuma equivalent is four visits to a modal dialog. It is faster the first time. It leaves no record of what you did, no way to review it, and no way to reproduce it on a fresh host except from a database backup.
Gatus’s conditions are better than they look
Look again at that nextcloud block. Four conditions, and every one of them checks something Uptime Kuma cannot express in a single monitor.
[STATUS] == 200 is the obvious one. [RESPONSE_TIME] < 800 turns “slow” into “down”, which is often what you actually want — a service taking nine seconds to respond is broken from a user’s point of view, and Uptime Kuma will cheerfully call it up. [BODY].installed == true reaches into the JSON response and checks a field. And [CERTIFICATE_EXPIRATION] > 240h fails the check ten days before a certificate expires, which converts an outage into a chore.
The condition language covers more: [BODY].items[0].name, len([BODY].results) > 0, [BODY] == pat(*healthy*), [IP] == 192.168.1.50. It is a small expression language and it is the reason a Gatus check can encode “this service is working” rather than “this service answers the phone”.
Uptime Kuma has keyword matching and a JSON query option, and they cover the common cases. Composing four conditions into one verdict is where the gap opens.
Where Uptime Kuma wins
The UI, obviously. Response time graphs, an event log per monitor, a clickable history. Gatus’s UI is a grid of coloured squares and a sparkline. It is beautiful in a spartan way and it tells you less.
Ninety notification providers vs about thirty. Both cover the ones you need. Kuma covers the ones you have never heard of.
Maintenance windows. Gatus has maintenance in the config with a start time and duration, and it applies globally. Uptime Kuma lets you schedule per-monitor windows from the UI, on a recurring schedule, without editing anything. If you do planned maintenance often, this is a genuine difference.
Push monitors. Uptime Kuma’s dead-man’s-switch — where your cron job calls it — has no direct Gatus equivalent. You can approximate it with an endpoint that checks a timestamp file over HTTP, and it is a workaround. For anyone whose main worry is “did the backup run?”, this alone decides it. The case for that monitor is in Uptime Kuma: monitoring that takes ten minutes.
Docker container monitors. Kuma can watch containers via the socket. Gatus watches network endpoints and that is all.
It’s approachable. Someone else in the house can look at the status page, and if they had to, could add a monitor. The YAML file is mine forever.
Where Gatus wins
Resources. Gatus is a Go binary. Mine sits at about 20 MB of RAM watching twenty endpoints. Uptime Kuma is Node.js and sits around 90–150 MB. On a machine with 32 GB, this is a rounding error. On a Raspberry Pi doing three other jobs, it is a real consideration.
Startup and reload. Gatus starts in under a second and watches its config file, so an edit is live almost immediately. There is no database migration on upgrade, because the schema is trivial and the config is the source of truth.
The condition language, as above.
Reproducibility. Delete the container, delete the volume, restore the YAML from git, start it. Your monitoring is back, byte-identical, minus the history. Doing the same with Uptime Kuma means restoring the SQLite file, which you had better have been backing up, and which is a binary blob that tells you nothing when you look at it. Both need backups; only one of them can be reconstructed from something a human wrote.
It scales down and up the same way. Twenty endpoints and two hundred endpoints are the same YAML file, and generating that file from a template or a script is trivial. Two hundred monitors through a web form is a lost weekend.
The status page itself
The title says status pages, so let’s talk about the page.
Gatus has one page and it is the application. Every endpoint, grouped by the group key, rendered as a row of coloured squares — one per recent check — with a response-time sparkline. Click an endpoint and you get its recent results with the actual condition outcomes. There is no separate “public page” concept; there is the page, and you decide who reaches it.
You shape it in the same YAML:
| |
Uptime Kuma separates the two. The dashboard is the admin interface behind a login, and a status page is a curated, published subset — chosen monitors, grouped, with a custom slug or domain, an incident banner, and a description. You can run several: one for the household, one for you.
That separation matters if anyone else looks at it. A household status page saying “Photos: up, Films: up, Backups: ran 4 hours ago” answers the question people were going to ask you anyway, and it exposes none of the admin surface. Gatus’s single page can be locked with basic auth, and the granularity stops there — everyone who gets in sees everything.
For a solo homelab, Gatus’s one page is enough and the lack of ceremony is a feature. The moment a second human is a consumer, Kuma’s incident banner and curated subsets start earning their weight.
Both speak Prometheus
Worth knowing before you commit to either: they both expose their state as Prometheus metrics. Gatus does it at /metrics with metrics: true in the config; Uptime Kuma does it at /metrics behind an API key.
So neither is a dead end. Start with whichever suits you, and if you later build a real metrics stack, both become a data source in it rather than something you rip out. The status checks answer “is it up”; Prometheus answers “why”, and having both feed one Grafana is a reasonable end state — Prometheus and Grafana without the enterprise baggage covers what that costs you.
Side by side
| Gatus | Uptime Kuma | |
|---|---|---|
| Configuration | YAML, git-native | web UI, SQLite state |
| RAM at rest | ~20 MB | ~90–150 MB |
| Condition logic | multi-condition expression language | status / keyword / JSON query |
| Response-time SLO as a check | yes, first-class | no |
| Cert expiry alerting | condition, any threshold | fixed warning days |
| Push / dead-man’s switch | no | yes |
| Docker container checks | no | yes |
| Maintenance windows | global, in config | per-monitor, scheduled |
| Graphs & history UI | minimal | good |
| Prometheus endpoint | yes | yes |
| Notification providers | ~30 | ~90 |
Troubleshooting both
Everything is down inside Docker and up from your laptop. Container DNS. Neither tool resolves your LAN’s internal names unless the container’s resolver knows them; Docker’s embedded DNS forwards to the host’s resolver, which may not be your split-horizon one. Set dns: in the compose file explicitly. This accounts for most first-run failures on both tools, and the underlying mechanic is in split-horizon DNS at home.
Gatus reports a condition failure with no detail. It tells you which condition failed and what the value was — hover the failing square, or check the logs, where it prints the evaluated expression with the actual values substituted. That output is the single best debugging feature either tool has.
[CERTIFICATE_EXPIRATION] is always 0. That condition only populates on https:// endpoints. On a plain HTTP URL it silently evaluates to zero and your check fails forever with no obvious reason.
Alerts fire on every blip. Both tools default to alerting on the first failure. In Gatus, set failure-threshold: 3 and success-threshold: 2 in the default-alert block; in Kuma, set Retries to 2 or 3 per monitor. Do this before you do anything else, or you will mute the tool within a week and then it is worth nothing.
Alerts arrive and you cannot act on them. Both will happily notify through a channel that depends on the infrastructure they are watching. The alert about your server being down should never need your server. Mine go to ntfy for routine things and an external channel for the monitors watching the infrastructure itself.
Neither tool notices the thing that actually broke. Both check reachability. A service can be up, answering 200, and returning an empty library because its database volume did not mount. Gatus can catch this if you write a body condition that checks something meaningful. Kuma can catch it with a keyword. Both need you to have thought about it, and the default check on a homepage URL will not.
Verdict
Run Gatus if your homelab already lives in a git repository and you would be annoyed by a component that does not. The YAML file is the feature. The 20 MB and the sub-second reload are pleasant; the fact that your entire monitoring configuration is a reviewable, greppable, reproducible artefact is the actual reason.
Run Uptime Kuma if you want the graphs, if you need push monitors for cron jobs, or if anyone other than you might ever look at it. The UI is a real advantage, and the push monitor catches a class of failure Gatus cannot see at all. It is also, honestly, the better first monitoring tool for almost everyone — the whole argument for it is in Uptime Kuma: self-hosted monitoring.
Running both is defensible and I did it for a while. Gatus watched the infrastructure with tight response-time conditions and lived in git; Kuma held the push monitors and the status page the household used. Two tools for one job is a smell, and it also meant each one did the thing it is good at.
I eventually consolidated on Gatus, because the day I rebuilt the monitoring host and the entire thing came back from a git clone and a docker compose up, with no database restore and no clicking, settled the argument for me. Your temperament may settle it differently, and that is a perfectly good reason.




