system-design

Monitoring and Alerting That Doesn’t Suck

An alert should mean “a human needs to act, now.” Most alerting systems fail this test, and the result — alert fatigue — is more dangerous than having no alerts at all.

Prerequisites: The Three Pillars, Availability Time to read: ~18 minutes


The problem

You set up monitoring. Soon: 200 alerts a day, most of them noise. People start ignoring the pager. Then a real incident fires an alert that looks exactly like the 199 false ones, and nobody acts for 40 minutes.

🚨 Alert fatigue is the central failure of monitoring, and it’s worse than under-alerting — because a team drowning in noise is trained to ignore alerts, so the real one is missed. The goal isn’t more alerts; it’s alerts that mean something.

The rule that fixes almost everything: “Every alert must be actionable and urgent. If a human doesn’t need to do something right now, it’s not an alert — it’s a dashboard or a ticket.”


Alert on symptoms, not causes

🚨 The single most important principle, and the one that separates good alerting from bad.

Cause-based alerting — alert on internal conditions: high CPU, a full disk, a service down, high memory.

Symptom-based alerting — alert on what users experience: elevated error rate, high latency, failed checkouts.

Why symptoms win:

❌ Cause-based:  "CPU is at 95%"
   → But users are fine (it's a batch job). False alarm, 3am, nobody needed to wake up.
   → OR you didn't alert on the CPU pattern that mattered, and users suffered silently.

✅ Symptom-based: "Checkout error rate is 8%, up from 0.1%"
   → Users are definitely affected. Someone needs to act. Always meaningful.

🚨 The deep reason: you can’t enumerate every cause of a user-facing problem, but you can measure the user-facing problem directly. A checkout might fail from high CPU, a full disk, a bad deploy, a dependency outage, a network issue, or a cause you’ve never seen — you can’t alert on all of them, but you can alert on “checkouts are failing.” Symptom alerts catch problems you didn’t anticipate, which are exactly the ones that hurt.

Causes belong on dashboards — you look at CPU when investigating a symptom alert, not as an alert itself.

🎙️ “I’d alert on symptoms — user-facing error rate and latency — not causes like CPU. You can’t enumerate every cause of a failure, but you can measure the failure directly, and it means every alert corresponds to real user impact.”


What to alert on: SLO burn rate

🚨 The modern approach, and a strong thing to bring up.

Instead of static thresholds (“alert if latency > 500ms”), alert on burning through your error budget too fast.

If your SLO is 99.9% availability, you have an error budget of 0.1% — about 43 minutes/month. → SLIs, SLOs, SLAs

Burn-rate alerting fires when you’re consuming that budget faster than sustainable:

Fast burn:  consuming a month's budget in an hour → page immediately (major incident)
Slow burn:  consuming it steadily over days → a ticket, not a page (a real but non-urgent problem)

Why this is better than static thresholds:

This is Google’s SRE approach and it directly attacks alert fatigue by making alerts proportional to real impact.


The alert quality checklist

Every alert should be:

Property Meaning
Actionable There’s a specific action to take. If not, it’s a dashboard.
Urgent It needs action now. If it can wait until morning, it’s a ticket.
User-impacting It reflects a real problem for users (symptom-based).
Documented 🚨 A runbook — what to check, what to do. An alert with no runbook wastes the responder’s night.
Rare If it fires constantly, either fix the underlying issue or the alert is wrong.
Owned Routed to whoever can actually fix it.

🚨 Every alert must link to a runbook. Being paged at 3am with “error rate high” and no guidance is how incidents drag on. The runbook says: here’s what this means, here’s what to check first, here’s how to mitigate.


Tiering: page vs ticket vs dashboard

🚨 Not everything is a page. Route by urgency:

Getting this tiering right is most of what makes alerting sustainable — a page at 3am should be a genuine “the building is on fire” event, not “CPU touched 80%.”


Avoiding alert fatigue

Concrete techniques:


Dashboards done right

Dashboards are for investigating (once an alert fires) and for situational awareness, not alerting.

Good dashboards:

A common anti-pattern: the “wall of graphs” — 60 panels nobody reads, that answer no specific question. A dashboard should answer “is the system healthy?” at a glance and support drilling into “why not?”


On-call and human factors

Monitoring exists to be acted on by humans, so the human system matters:


⚖️ Trade-offs

Choice Gain Cost
Symptom-based alerts Every alert is real user impact; catches unknown causes Doesn’t tell you the cause (that’s the dashboard’s job)
Burn-rate alerts Proportional to real impact; fewer false alarms More complex to set up than static thresholds
Aggressive alerting Catch everything early Alert fatigue; ignored pages; burnout
Conservative alerting Sustainable, meaningful pages Might miss an early signal
Multi-window Ignores brief blips Slightly slower to fire on real issues

🚨 The core tension: sensitivity vs specificity. Alert on everything and you get fatigue; alert on too little and you miss incidents. Symptom-based, burn-rate, multi-window alerting is how you get high specificity (few false alarms) without sacrificing sensitivity to real problems.


In the real world


🚨 Interview traps


🎙️ Soundbites


🛠️ Try it

1. Build a symptom-based alert. Instrument a service’s error rate, then alert on “error rate > 2% for 5 minutes” rather than on CPU. Trigger it by making requests fail, and confirm the alert corresponds to real (simulated) user impact. Then spike the CPU with a batch job and confirm it does not alert — because users are fine.

2. Implement burn-rate alerting. Define an SLO (99.9%), compute the error budget, and alert on fast vs slow burn with different severities. Simulate a brief blip (shouldn’t page) and a sustained problem (should page). Watching the multi-window logic suppress the blip is the “aha.”

3. Audit real alerts. Take any monitoring setup and list every alert. For each, ask: is it actionable? urgent? does it have a runbook? has it ever led to action? Most setups have several alerts that fail this test — that’s your noise to delete.

4. Cause an alert storm. Make a shared dependency (database) fail and watch how many alerts fire across dependent services. Then implement dependency-aware suppression so the root cause pages once, not fifty times.


Check yourself

1. Why alert on symptoms rather than causes? Because symptoms (user-facing error rate, latency, failed transactions) directly measure real impact, while causes (CPU, memory, disk) often don't correspond to user impact — high CPU might be a harmless batch job, so alerting on it produces false alarms that train people to ignore the pager. More fundamentally, you cannot enumerate every possible cause of a user-facing problem: a checkout can fail from a bad deploy, a dependency outage, a network issue, a full disk, or a cause you've never seen. You can't write an alert for each, but you *can* alert on "checkouts are failing," which catches the problem regardless of cause — including the causes you didn't anticipate, which are exactly the ones that hurt. Causes belong on dashboards, consulted when investigating a symptom alert, not as alerts themselves.
2. What is alert fatigue and why is it worse than under-alerting? Alert fatigue is what happens when a system generates so many alerts — especially noisy, non-actionable ones — that responders become desensitized and start ignoring them. It's worse than under-alerting because it actively *trains* the team to disregard the pager: when a real, urgent incident finally fires an alert, it looks identical to the hundreds of false ones people have learned to dismiss, so nobody acts, and the real problem festers. Under-alerting at least leaves people attentive to the alerts that do fire. The dangerous state isn't "no alerts," it's "alerts that have been rendered meaningless by noise." This is why the goal of alerting is *fewer, better* alerts — every one actionable and urgent — not more coverage. It's a documented, serious failure mode (with fatal parallels in hospital alarm fatigue).
3. What is SLO burn-rate alerting and why is it better than static thresholds? Instead of alerting when a metric crosses a fixed number ("latency > 500ms"), burn-rate alerting fires when you're consuming your error budget — the allowable failure implied by your SLO — faster than sustainable. If your SLO is 99.9%, you have ~43 minutes of failure budget per month; a "fast burn" (consuming a month's budget in an hour) pages immediately, while a "slow burn" (consuming it over days) becomes a ticket. It's better because: the threshold is tied to *actual user impact* (the SLO) rather than an arbitrary guessed number; it's multi-window, so a brief blip that recovers doesn't fire while a sustained problem does — dramatically reducing false alarms; and severity follows burn rate, so urgency is proportional to real impact. It directly attacks alert fatigue by making every alert meaningful.
4. What's the difference between a page, a ticket, and a dashboard, and why does it matter? A **page** wakes someone up: it's for urgent, user-impacting problems needing immediate human action (checkout down, data-loss risk), and it should be *rare* — a well-tuned system pages a few times a month. A **ticket** is for real but non-urgent problems handled during business hours (slow error-budget burn, a degraded non-critical feature, approaching a capacity limit). A **dashboard** is for causes, resource metrics, and trends — consulted when investigating, not an alert at all. The distinction matters because it's most of what makes alerting sustainable: if everything pages, you get fatigue and burnout; if urgent things become tickets, you miss incidents. Routing each signal to the right tier — reserving pages for genuine emergencies — keeps the pager meaningful and the team functional. A 3am page should feel like "the building is on fire," not "CPU touched 80%."
5. How do you prevent a cascading failure from producing an alert storm? Two main techniques. **Alert grouping/deduplication**: one incident that trips many checks should generate a single notification, not one per check — 50 correlated alerts for one root cause are useless (and drown any other signal). **Dependency-aware suppression**: when a root cause is already alerting (the database is down), suppress the downstream alerts from the services that depend on it, since they're all symptoms of the same problem — you want one page saying "database down," not 30 pages saying "service X can't reach database." Additionally, multi-window burn-rate alerting avoids firing on the transient spikes a cascade produces, and auto-resolution clears alerts once conditions recover. The goal is that the responder gets one clear, actionable page pointing at the actual cause, rather than a wall of correlated noise they have to sift through under pressure.

Further reading