“On-call got paged 64 times last night. None of the pages needed a human. At 06:10 a real one was acknowledged and ignored.”
case name: 64 pages, zero actions
- Service
- alerting
- Impact
- A real checkout outage went unhandled for 35 minutes because the page looked like the other 63
- Detected by
- The morning after, in the incident review
- Time to resolve
- A week of cleanup
Skills you'll use on this case
amtool00 It starts
Reproduce this incident in your lab, then work the case alongside the timeline below. Try each query yourself before reading its result.
01 The investigation
- 08:30
QUERY
Which alerts fired, and how often?
Prometheus exposes its own alert state as the
ALERTSseries. Counting firing samples per alert over the night ranks the noisiest ones.PromQL· Prometheussort_desc(count by (alertname) (count_over_time(ALERTS{alertstate="firing"}[12h])))result{alertname="HighCPU"} 412 {alertname="ShopLiteRestartingFrequently"} 96 {alertname="PodMemoryAbove80Percent"} 88 {alertname="CheckoutErrorRatioHigh"} 7 {alertname="DiskWillFillIn4Hours"} 3 - 08:40
FINDING
The noise was about CAUSES, not SYMPTOMS
HighCPU fired during nightly batch jobs, when high CPU is the whole point. The memory alert fired because the service runs close to its limit by design. Restart alerts fired during a planned node rotation. None of them meant a user was affected.
CheckoutErrorRatioHigh, the one that did, looked exactly like the others and got the same tired acknowledgment. - 08:55
FINDING
Every alert went to the same pager with the same urgency
No severity routing, no grouping, no inhibition. When ShopLite went down, eleven different alerts fired about it separately, one per metric.
- Next week
RESOLVED
Alerts triaged: 4 page, 9 ticket, 23 deleted; Alertmanager routes by severity
Root cause
Alerts had grown one incident at a time with no policy. Most watched internal causes (CPU, memory, restarts) rather than user-facing symptoms, all of them paged, and nothing grouped or suppressed related alerts. The resulting noise trained on-call to ignore pages, including the real one.
02 The concepts behind it
What deserves a page
A page wakes a human, so it must be URGENT (needs action now), ACTIONABLE (there's something they can do), and REAL (users are affected or about to be). That almost always means SYMPTOMS: error rates, latency, availability, or imminent resource exhaustion that WILL cause symptoms (disk full within hours). Causes like high CPU are useful on dashboards and for diagnosis, not as pages.
Everything else becomes a TICKET (fix during working hours) or is deleted. A useful review question for each alert: 'The last 10 times this fired, what did the on-call person do?' If the answer is 'nothing', it goes.
Alertmanager: Prometheus decides, Alertmanager delivers
Prometheus evaluates rules and sends firing alerts to Alertmanager. Alertmanager ROUTES them (a tree of matchers on labels, such as severity="critical" → pager), GROUPS related alerts into one notification (group_by: [service]), INHIBITS alerts made redundant by a bigger one (ShopLiteDown suppresses every other shoplite alert), and applies SILENCES during planned work. Labels on the rules are what make all of this possible.
Timing knobs
group_wait (how long to wait to batch the first alerts of a group, e.g. 30 s), group_interval (how often to send updates about a group), and repeat_interval (how often to re-notify about an alert that's still firing, e.g. 4 h). Plus for: on each rule. Together they turn a storm of flapping alerts into one readable message.
03 The fix
01Add Alertmanager and a webhook logger to the lab
The logger is an echo server that prints every notification it receives to stdout. Alloy ships that to Loki, so you can see exactly what 'got paged' with
{container=~".*alert-logger.*"}. Tell Prometheus where Alertmanager is.obs-lab/docker-compose.yml + prometheus.ymladd to fileyaml # docker-compose.yml alertmanager: image: prom/alertmanager:v0.28.1 command: --config.file=/etc/alertmanager/alertmanager.yml volumes: ["./alertmanager:/etc/alertmanager:ro"] ports: ["9093:9093"] alert-logger: image: mendhak/http-https-echo:37 # prometheus/prometheus.yml alerting: alertmanagers: - static_configs: [{ targets: ["alertmanager:9093"] }]02Route by severity, group by service, inhibit when the service is down
Default route: tickets. Critical alerts go to the pager. Alerts for the same service are grouped into one notification. When
ShopLiteDownfires, every other shoplite alert is inhibited, since one clear page beats eleven vague ones.obs-lab/alertmanager/alertmanager.ymlwhole fileyaml route: receiver: ticket group_by: [alertname, service] group_wait: 30s group_interval: 5m repeat_interval: 4h routes: - matchers: [severity="critical"] receiver: pager group_by: [service] inhibit_rules: - source_matchers: [alertname="ShopLiteDown"] target_matchers: [service="shoplite", alertname!="ShopLiteDown"] equal: [service] receivers: - name: pager webhook_configs: [{ url: "http://alert-logger:8080/pager" }] - name: ticket webhook_configs: [{ url: "http://alert-logger:8080/ticket" }]terminal$ docker compose exec alertmanager amtool config routes test \--config.file=/etc/alertmanager/alertmanager.yml severity=critical service=shoplite── expected output ──pager03Re-label the rules you keep; delete the rest
severity: criticalonly for user-facing symptoms: CheckoutErrorRatioHigh (burn-rate in Case 4.3) and ShopLiteDown (Case 4.4). Warnings like memory prediction, event-loop lag, and retry amplification becomeseverity: warningand go to tickets. Delete HighCPU.04Silence planned work
Before a maintenance window, silence matching alerts for a fixed duration with a comment. Everyone can see who silenced what and why in the Alertmanager UI.
terminal$ docker compose exec alertmanager amtool silence add service=shoplite \--alertmanager.url=http://localhost:9093 --duration=1h --comment="node rotation, ticket OPS-412"── expected output ──b7e3d1a2-4c5f-4e8a-9d0b-1f2e3a4b5c6d
04 Make sure it never surprises you again
01Measure your paging load
Track notifications per receiver per week, and review them in the on-call handover. A common target is fewer than two pages per on-call shift. If it's higher, delete or demote alerts before adding new ones.
PromQL· Prometheussum by (receiver) (increase(alertmanager_notifications_total{integration="webhook"}[7d]))
05 Your turn: write the query
Run each one against your lab before revealing the answer. Share your own version in the comments; there's usually more than one correct query.
Switch on the errors scenario. Which receiver gets the notification, and how do you see what was sent?
Stop shoplite entirely (docker compose stop shoplite) with ShopLiteDown defined (Case 4.4). Which alerts reach a receiver, and why?
06 Interview questions from this case
How do you reduce alert fatigue?
What does Alertmanager do that Prometheus doesn't?