Skip to main content

Grafana OnCall Is Dead. Here Is the $0 Paging Stack for a Two-Person Platform

13 min readDora NodaDora Noda
Share
On this page

On March 24, 2026, the Grafana OnCall open-source repository went read-only. No drama, no fork-worthy scandal — just a maintenance-mode notice that had been counting down for a year, and a pointer toward Grafana Cloud IRM. If you were a platform team with a Grafana Cloud contract, this was a migration ticket. If you were one or two people running your own infrastructure, it was the quiet removal of the only self-hostable answer to PagerDuty most small teams had ever heard of.

So here is the verdict up front: a two-person team can replace everything it actually used OnCall OSS for with three self-hosted pieces that cost nothing to run — Uptime Kuma probing from outside the fleet, Prometheus Alertmanager routing by severity, and ntfy for last-mile push to your phone — plus one free external heartbeat so the monitoring stack cannot die silently. That is the whole post in one paragraph. The rest is the wiring diagram, the config that makes it real, and the honest list of what you give up.

LayerToolDirectionJob
Outside-in probingUptime KumaOutside → inBlack-box checks: is the thing up, is TLS valid, did the cron job phone home
Inside-out routingPrometheus AlertmanagerInside → youSeverity routing, grouping, and quiet hours for everything Prometheus fires
Last-mile pushntfy (self-hosted)Alert → pocketPhone notification from a curl POST; no SDK, no key, no third party
The watcher-watcherFree external heartbeatFleet → outsideDead-man's-switch ping so a dead monitor pages you instead of going quiet

Two things to read off this table before the detail. First, each layer watches a different failure direction — external reachability, internal signal quality, and delivery to a human — which is why one tool never covered all three. Second, the fourth row is not optional garnish. It is the load-bearing piece, and it is the one almost nobody builds until the first silent outage teaches them.

The hole Grafana left, priced out

Grafana's timeline is worth stating exactly, because half-remembered versions of it are already circulating. On March 11, 2025, Grafana Labs put OnCall OSS into maintenance mode — critical bug fixes and security fixes only, no feature work — with archiving scheduled a year out. On March 24, 2026, the grafana/oncall repository was archived to read-only, and active development continued exclusively in Grafana Cloud IRM. The docs pages for the OSS setup now all carry the same banner. This was not a rug-pull; it was a slow, well-signposted commercial consolidation. But intent does not change the outcome for self-hosters: the project's own recommended path is a cloud product with a per-user price.

And the per-user prices are the second half of the hole. PagerDuty's Professional plan lists around $21 per user per month, Business around $41, with automation and analytics as add-ons that push real bills higher. Opsgenie historically sat near $9 per user per month for the standard tier, but it now lives inside Atlassian's Jira Service Management bundling, which is priced for teams that want the whole Atlassian suite, not for two people who want their phones to buzz. None of these vendors did anything wrong — on-call scheduling, escalation chains, and phone-call fallback for a 200-person rota is genuinely worth $40 a seat. The problem is only that the pricing assumes a rota you do not have. Two people do not need round-robin scheduling across time zones. They need: wake me up when it is broken, stay quiet otherwise, and never fail silently.

That is a small enough requirement that the replacement stack can be genuinely free — free as in zero marginal dollars, running on machines you already pay for, pushing to apps that cost nothing and phone home to nobody.

Layer 1: Uptime Kuma as the outside eyes

Uptime Kuma is the most-starred self-hosted monitoring tool on GitHub for a reason: it answers the single most important monitoring question — "from the internet's point of view, is my stuff reachable?" — with a setup flow that takes minutes. It is a single Node.js service with an SQLite backend, and it runs happily in one small container.

Its monitor list covers HTTP(S) endpoints, keyword and JSON-query checks on responses, TCP ports, ping, DNS records, Docker container health via a read-only socket mount, TLS certificate expiry, and Push-type monitors that invert the direction (more on those in the heartbeat section).

Two properties make it the right outside-eyes layer rather than just a status page. First, its notification integrations number over ninety — Telegram, Discord, Slack, Pushover, Gotify, Matrix, webhooks, and plain email among them — so wherever the rest of your stack decides to send an alert, Kuma can usually deliver it without a new account. Second, it is itself a perfectly good alerting endpoint for the dumbest, most robust class of checks: synthetic probes on a timer. Your Prometheus can be down, your cluster can be mid-upgrade, your entire internal pipeline can be on fire, and Kuma — sitting on a different machine, ideally a different network — keeps hitting your public endpoints every 60 seconds and telling the truth about them.

Placement is the whole game here, so say it explicitly: Kuma must not live on the infrastructure it monitors. A Kuma instance on the same cluster whose outages it is supposed to catch is a status page that goes down with the site. The standard small-team answer is a cheap VPS from a different provider in a different region than your fleet — the kind of box that costs less per month than one PagerDuty seat — or, at minimum, a separate host outside the cluster's failure domain. Externality is the feature; everything else is configuration.

Layer 2: Alertmanager severity routing

If Kuma is the outside eyes, Alertmanager is the inside brain — and you almost certainly already run it, because it ships with every kube-prometheus-stack install. The mistake small teams make is treating its default configuration as finished: one receiver, everything to one channel, every alert equally loud. At 3 a.m., "TLS cert expires in 20 days" arriving with the same violence as "all API pods crashlooping" is how people learn to mute the channel, and a muted channel is the same as no monitoring.

The fix is a route tree with two destinations and honest thresholds. Critical alerts — the ones that mean users are hurt right now — page immediately with short repeat intervals. Everything else digests to a channel you read over coffee:

yaml
route:
  receiver: "ntfy-warnings"
  group_by: ["alertname", "severity"]
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
    - matchers: [severity = "critical"]
      receiver: "ntfy-critical"
      group_wait: 10s
      repeat_interval: 5m
      continue: false
 
receivers:
  - name: "ntfy-critical"
    webhook_configs:
      - url: "https://push.example.com/alerts-critical"
  - name: "ntfy-warnings"
    webhook_configs:
      - url: "https://push.example.com/alerts-warnings"

Three details carry the weight here. group_by with group_wait means a flapping deployment produces one grouped notification, not forty. The short repeat_interval on critical (five minutes, not four hours) means a paged human who acknowledged nothing gets reminded — the page that fires once and never repeats is how outages get slept through. And both receivers are plain webhook_configs pointed at ntfy topics, which is the entire integration: Alertmanager already speaks HTTP POST, and ntfy already accepts it. No exporter, no adapter service, no plugin to keep updated.

The discipline that makes this work is not in the YAML — it is the rule that nothing ships with severity="critical" unless a human should be woken up. Every alert that pages but should not have is a withdrawal from a trust account with exactly two holders. Audit the critical label quarterly; demote anything that routinely fires without action.

If warnings should stay silent overnight, a time_intervals mute timing on the warnings receiver is five lines of config — genuine quiet hours without touching the critical path. A two-person team cannot afford alert fatigue because there is nobody else to absorb it.

Layer 3: ntfy, the last mile that phones home to nobody

ntfy is an open-source, HTTP-based push notification service with the smallest possible API surface: POST a body to a topic URL and every subscriber of that topic gets a push. No SDK, no API key dance, no Firebase project, no Apple developer enrollment for the basic path:

bash
curl -d "API cluster prod-1: CrashLoopBackOff on payments-api" \
  https://push.example.com/alerts-critical

That is the entire client library, and it is why ntfy is the right last mile for this stack — Alertmanager's webhook receiver and a shell one-liner speak the same protocol, so every layer above can deliver to your pocket without new code.

Self-hosting it is a single Go binary with an SQLite backend, behind whatever reverse proxy terminates your TLS. Two honest caveats, since this post promises no hand-waving.

First, topics are capability-URLs: anyone who can guess the topic name can read and publish to it, so generate long random topic names and put authentication on the server for anything sensitive. Second, iOS push has one real wrinkle — Apple's push infrastructure means a self-hosted instance needs its public base URL configured correctly plus an upstream reference for iOS delivery, a documented one-time setup step, not a recurring tax. Android and web work with no such step.

Neither caveat changes the economics: the server costs nothing beyond the box it shares with something else, the apps are free, and no notification content ever transits a vendor's cloud.

The practical shape for a two-person team is two topics mirroring the Alertmanager split — alerts-critical subscribed with an alarm-style sound and bypass for Do-Not-Disturb, alerts-warnings subscribed quietly — so the severity decision made in the route tree survives all the way to the lock screen. Routing that collapses at the last mile was never routing at all.

Who watches the watcher

Now the failure mode nobody designs for. Every component above except the external heartbeat lives on infrastructure you operate: Kuma on its VPS, Prometheus and Alertmanager in the cluster, ntfy on a box somewhere. When the fleet has a bad day — the VPS provider has an outage, the cluster's control plane wedges, the reverse proxy in front of ntfy goes down — the monitoring stack does not page you about its own death. It just goes quiet. And quiet is indistinguishable from healthy, which is the most dangerous property a monitoring system can have.

The standard fix is a dead-man's switch: invert the direction so that silence is the alert. Your fleet actively pings an external check on a schedule, and the external service pages you when pings stop arriving. Uptime Kuma's Push monitor type speaks exactly this protocol — your cron jobs, your backup scripts, and a small heartbeat sidecar next to Alertmanager all POST to a push URL on each successful run, and Kuma fires when a heartbeat goes stale. Healthchecks.io and similar hosted checks offer the same primitive with a free tier, if you prefer the watcher-of-last-resort to be someone else's uptime.

The recipe has three parts, and all three matter:

  1. The heartbeat must originate inside the failure domain you fear. A heartbeat from Kuma about Kuma proves nothing. The ping that matters comes from the cluster (a CronJob that curls the push URL every five minutes) and from the backup path (every backup script pings on success), so their silence means the thing you care about stopped working.
  2. The receiver must live outside that failure domain. If Kuma is your receiver, it stays on the off-fleet VPS from the placement section. If you would rather not trust one VPS, a hosted check's free tier is the cheapest second opinion in monitoring.
  3. The heartbeat's own alert path must not depend on the fleet. The stale-heartbeat notification should go through a channel that survives your infrastructure being down — Kuma's direct notifier, or the hosted check's email — never through the self-hosted ntfy instance that might be down alongside everything else.

Note the recursion terminates after one level: the external check is simple enough (one endpoint, one timer) that its own failure modes are visible — you notice when the heartbeat service itself has an incident because its status page is someone else's problem to maintain. One external dependency, chosen deliberately, is infinitely better than zero external dependencies and a silent fleet.

What you give up, stated plainly

This stack does not replace PagerDuty; it replaces the subset of PagerDuty two people used. The gaps are real and you should know them before adopting it:

  • No schedules or escalations. There is no "page Alice, then Bob after ten minutes." There is only "page the topic," and the topic is both of your phones. For two people who sit within shouting distance of each other — literally or on Slack — this is fine. It stops being fine around person five.
  • No phone-call fallback. ntfy delivers push notifications, not circuit-switched voice calls. A phone on Do-Not-Disturb with the critical topic allowed through is close, but "close" is doing work in that sentence during a real outage. If your workloads need guaranteed voice wake-ups, budget for a call-capable provider for the critical topic only.
  • No audit trail or incident timeline. PagerDuty's unsexy superpower is the record: who was paged, when they acknowledged, what happened in what order. This stack gives you scattered logs across three tools. For regulated workloads or postmortem culture, that gap matters.
  • You operate it. Version upgrades, disk space on the Kuma box, TLS renewal for the ntfy endpoint — all yours. The honest accounting is that this costs perhaps an hour a quarter, versus the per-seat subscription forever.

The graduation rule follows directly: when you hire the third on-call human, or when an auditor asks for the acknowledgement log, or when a missed push costs more than a year of PagerDuty Professional — buy the commercial tool, keep Kuma and the heartbeat as the independent second opinion, and feel no shame about it. Self-hosting the paging stack is the right default for a team of two, not a moral commitment for a team of twenty.

The shape of the whole thing

One VPS outside your fleet runs Uptime Kuma, hitting public endpoints every minute and holding the Push-monitor dead-man's switches. Inside the fleet, Prometheus fires and Alertmanager routes: critical pages now and repeats every five minutes, warnings digest every four hours. Both receivers send an HTTP POST to self-hosted ntfy topics, and your phones subscribe with different interruption levels. A five-minute CronJob heartbeat plus per-backup pings keep the dead-man's switch fed, and the stale-heartbeat alert travels a path that survives your fleet being down. Total marginal cost: one small VPS and an hour a quarter. Total vendors with your notification data: zero.

That is what Grafana OnCall OSS used to give a small team — minus the schedules they never configured, minus the escalation chains with two entries, and minus the per-seat bill for a rota that was just two tired humans with phones.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex