Alerting
An alert is a request for a human's attention, often at 3 a.m. That makes it the most expensive signal in the system: it costs sleep, focus and, over time, trust. Two failure modes kill alerting. Too many alerts β "CPU above 80%" on every pod, every blip of every dependency β and the on-call engineer learns to ignore the pager; the real incident drowns in the noise. Too few, or the wrong ones β and the first person to notice the broken checkout is a fan on social media. This page is about alerts that fire when, and only when, users are being hurt enough that someone must act.
What deserves a page
A good page is urgent, actionable, and about a user-visible problem1. If any of the three is missing, it should be something else:
| Situation | Urgent? | Actionable? | Users affected? | Destination |
|---|---|---|---|---|
| Checkout failing for 5% of fans during an on-sale | yes | yes | yes | page the on-call |
| Error budget burning slowly: will run out in 10 days | no | yes | a little | ticket for the team, this week |
| Disk of the Kafka broker will be full in 4 days at this rate | no | yes | not yet | ticket |
One of 30 catalog pods restarted and recovered | no | no | no | dashboard / log only |
| CPU at 85% while all SLIs are fine | no | no | no | nothing β or a capacity review |
| The alerting pipeline itself is broken | yes | yes | indirectly | page (via a dead man's switch, below) |
Symptoms, not causes
Alert on symptoms β what users experience: errors, latency, freshness, correctness. Use causes β CPU, memory, a pod restart, a full queue β for dashboards and diagnosis, not for pages.
| Symptom-based alert | Cause-based alert | |
|---|---|---|
| Example | "checkout success ratio below SLO" | "PostgreSQL CPU > 90%" |
| Fires when users are fine? | no | often (high CPU, happy users) |
| Misses real problems? | rarely β whatever the cause, users feel it | often β the failure you did not predict has no alert |
| Number of rules | few, per user journey | many, per component and resource |
| Tells you where to look? | no β that is what dashboards and traces are for | yes, but only for known failure modes |
The exception is a leading indicator that will certainly become a symptom and gives time to act β a disk filling up, a certificate expiring in 7 days. Those are tickets, not pages, unless the deadline is hours away.
From the SLO to the alert
The SLO already says how much failure is acceptable: with 99.9% of checkouts succeeding over a rolling 28-day window (the window used throughout Reliability), the error budget is 0.1% of checkouts. The question an alert must answer is therefore not "are there errors?" β there always are β but "are we spending the budget fast enough to be in trouble?".
The natural measure is the burn rate:
A burn rate of 1 spends exactly the whole budget in the whole window; a burn rate of 14.4 spends it 14.4 times faster. If a burn rate \(b\) is sustained for a time \(t\), the fraction of the budget consumed is
| Burn rate | Error ratio (SLO 99.9%) | Budget exhausted in | Budget spent in 1 h | in 6 h |
|---|---|---|---|---|
| 1Γ | 0.10% | 28.0 days | 0.1% | 0.9% |
| 2Γ | 0.20% | 14.0 days | 0.3% | 1.8% |
| 3Γ | 0.30% | 9.3 days | 0.4% | 2.7% |
| 6Γ | 0.60% | 4.7 days | 0.9% | 5.4% |
| 14.4Γ | 1.44% | 46.7 h | 2.1% | 12.9% |
| 36Γ | 3.60% | 18.7 h | 5.4% | 32.1% |
| 1000Γ | 100.00% | 40 min | 100.0% | 100.0% |
A burn rate of 1000Γ is a total outage (100% errors against a 0.1% budget): the whole 28-day budget is gone in about 40 minutes. That is why fast alerts must fire within minutes β and why slow burns can wait for office hours.
Six ways to write the alert
The SRE Workbook2 compares six progressively better designs for an SLO alert. Their quality is measured by four properties: precision (fraction of alerts that were real problems), recall (fraction of real problems that alerted), detection time and reset time (how long the alert keeps firing after the problem ends).
| # | Rule | Precision | Recall | Detection | Reset | Problem |
|---|---|---|---|---|---|---|
| 1 | error ratio > 0.1% over 10 min | low | high | fast | fast | pages for blips that barely touch the budget |
| 2 | error ratio > 0.1% over 36 h | high | high | very slow | very slow | a total outage takes ~2 min to alert, and the alert lasts 36 h |
| 3 | rule 1 + for: 1h | medium | low | slow | fast | a 100% outage still waits an hour |
| 4 | burn rate 14.4Γ over 1 h | high | medium | good | slow | slow burns (3Γ) never alert; reset takes up to an hour |
| 5 | several burn rates (14.4Γ / 6Γ / 1Γ) | high | high | good | slow | complex; still slow to reset |
| 6 | multiwindow, multi-burn-rate | high | high | good | fast | the most rules β generate them with a tool |
Multiwindow, multi-burn-rate
The recommended design combines, for each severity, a long window that proves the burn is significant and a short window (1/12 of the long one) that proves it is still happening β so the alert stops quickly once the problem is fixed:
| Severity | Long window | Short window | Burn rate | Budget spent when it fires |
|---|---|---|---|---|
| page | 1 h | 5 min | 14.4Γ | 2% |
| page | 6 h | 30 min | 6Γ | 5% |
| ticket | 3 days | 6 h | 1Γ | 10% |
The plot shows the detection time for a sudden failure of a given error ratio, for the two paging rules. Note the trade-off it encodes: the worse the incident, the faster the alert β and for a given rule, the budget spent at the moment it fires is always the same (2% and 5%).
A total outage (100% errors) fires the fast rule in under a minute; an incident failing 2% of checkouts takes about 43 minutes to fire the fast rule, and one failing 0.7% never fires it, but fires the 6-hour rule after about 5 hours. Below 0.6% β a burn rate under 6Γ β only the ticket rule catches it.
The rules in PromQL
Recording rules precompute the error ratio of the checkout journey over each window; the alert rules compare them with the thresholds. (Tools such as Sloth, Pyrra or the OpenSLO specification generate these rules from a short SLO definition, which avoids copy-paste mistakes.)
# prometheus-rules.yaml β checkout availability SLO: 99.9% over 28 days
groups:
- name: slo-checkout-recordings
rules:
- record: slo:checkout_errors:ratio_rate5m
expr: |
sum(rate(http_server_request_duration_seconds_count{job="order", http_route="/checkout", http_response_status_code=~"5.."}[5m]))
/
sum(rate(http_server_request_duration_seconds_count{job="order", http_route="/checkout"}[5m]))
- record: slo:checkout_errors:ratio_rate1h
expr: |
sum(rate(http_server_request_duration_seconds_count{job="order", http_route="/checkout", http_response_status_code=~"5.."}[1h]))
/
sum(rate(http_server_request_duration_seconds_count{job="order", http_route="/checkout"}[1h]))
# ... the same expression for 30m, 6h and 3d
- name: slo-checkout-alerts
rules:
- alert: CheckoutErrorBudgetFastBurn
expr: |
slo:checkout_errors:ratio_rate1h > (14.4 * 0.001)
and
slo:checkout_errors:ratio_rate5m > (14.4 * 0.001)
labels:
severity: page
team: checkout
annotations:
summary: "Checkout is burning its 28-day error budget at more than 14.4x"
runbook_url: "runbooks/checkout-slo.md"
- alert: CheckoutErrorBudgetSlowBurn
expr: |
slo:checkout_errors:ratio_rate6h > (6 * 0.001)
and
slo:checkout_errors:ratio_rate30m > (6 * 0.001)
labels:
severity: page
team: checkout
- alert: CheckoutErrorBudgetTicket
expr: |
slo:checkout_errors:ratio_rate3d > (1 * 0.001)
and
slo:checkout_errors:ratio_rate6h > (1 * 0.001)
labels:
severity: ticket
team: checkout
A latency SLO works the same way: the "bad events" are the requests slower than the threshold, computed from the histogram bucket that ends at the threshold:
# fraction of checkouts slower than 2 s over 1 h (the "error ratio" of a latency SLO)
1 - (
sum(rate(http_server_request_duration_seconds_bucket{job="order", http_route="/checkout", le="2"}[1h]))
/
sum(rate(http_server_request_duration_seconds_count{job="order", http_route="/checkout"}[1h]))
)
In the BoxOffice code: the catalog burn-rate alerts (lab 8)
Hands-on 8 β SLO and burn-rate alert writes the first two rules of the table above as Grafana alert rules, provisioned from a file: an SLO of 99.5% over 28 days for catalog (a budget of 0.5%), with the health checks excluded from the SLI because no fan made those requests. Query A is the burn rate over 1 h, query B (not shown) the same expression over 5 min, and C fires only when both exceed 14.4. Two choices differ from the table: the 6Γ rule is a ticket, not a page (a cheaper signal for a small team), and for: 1m adds a minute of confirmation. In the lab's test run, stopping the database fired the page after about 440 s, with burn rates of 17.0 (1 h) and 200.0 (5 min).
- uid: catalog-availability-fast-burn
title: "catalog availability SLO: fast burn"
condition: C
data:
- refId: A # burn rate over the long window (1 h)
relativeTimeRange: { from: 3600, to: 0 }
datasourceUid: prometheus
model:
refId: A
instant: true
expr: |
(
sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*", http_response_status_code=~"5.."}[1h]))
/ sum(rate(http_server_request_duration_seconds_count{service_name="catalog", http_route!~"/actuator.*"}[1h]))
) / 0.005
- refId: C
datasourceUid: __expr__
model:
refId: C
type: math
expression: "$A > 14.4 && $B > 14.4"
for: 1m
noDataState: OK
execErrState: Error
labels:
severity: page
slo: catalog-availability
BoxOffice β the 10:00 on-sale, through the alert's eyes
At 10:02 the payment provider slows down; at 10:03 the checkout error ratio (timeouts returned as 504) reaches 4%. That is a burn rate of 40Γ: the fast rule's 1-hour average crosses 1.44% after about 60 Γ 1.44 / 4 β 22 minutes of a constant 4%β¦ far too slow for an on-sale that is over in 30 minutes. The team therefore adds a dedicated on-sale SLO with a shorter window (for example 99% of checkouts under 2 s during the on-sale hour, alerting on 5-minute and 1-minute windows) β the general rules are tuned for a 28-day budget and cannot react to an event that lasts half an hour. Different journeys, different SLOs.
Routing: from a firing rule to a person
An alert rule only produces a firing alert. Something must decide who hears about it, how, and how often. In the Prometheus ecosystem this is Alertmanager3 (Grafana Alerting has the same concepts): it groups related alerts into one notification, deduplicates them, applies inhibitions and silences, and routes them by labels to receivers.
flowchart LR
R["Rule evaluation<br/>Prometheus / Mimir ruler"] --> F["Firing alert<br/>severity=page, team=checkout"]
F --> G["Alertmanager<br/>group Β· dedupe"]
G --> S{"Silenced or<br/>inhibited?"}
S -->|yes| X["Dropped<br/>(still visible in the UI)"]
S -->|no| RT{"Route by labels"}
RT -->|severity=page| PD["Pager<br/>on-call phone"]
RT -->|severity=page| SL["Slack #incidents"]
RT -->|severity=ticket| TK["Ticket queue"]
PD --> ACK["On-call acknowledges<br/>or escalates after 5 min"] # alertmanager.yaml (sketch)
route:
receiver: slack-observability # default for anything unmatched
group_by: [alertname, team]
group_wait: 30s # wait to batch related alerts into one notification
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severity="page"]
receiver: pager-checkout
continue: true # also evaluate the next route
- matchers: [severity="page"]
receiver: slack-incidents
- matchers: [severity="ticket"]
receiver: ticket-queue
inhibit_rules:
- source_matchers: [alertname="RegionDown"]
target_matchers: [severity=~"page|ticket"]
equal: [region] # one page for the region, not fifty for its services
receivers:
- name: pager-checkout
pagerduty_configs:
- routing_key_file: /etc/alertmanager/secrets/pager-checkout
- name: slack-incidents
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack-incidents
channel: "#incidents"
- name: slack-observability
slack_configs:
- api_url_file: /etc/alertmanager/secrets/slack-observability
channel: "#observability"
- name: ticket-queue
webhook_configs:
- url: http://ticket-bridge:8080/alerts
| Mechanism | What it does | BoxOffice use |
|---|---|---|
| Grouping | many alerts with the same labels β one notification | 40 order pods failing β one page, not 40 |
| Inhibition | an alert suppresses others while it fires | RegionDown silences the per-service alerts of that region |
| Silence | mute matching alerts for a period, with an author and a reason | planned database maintenance on Tuesday 02:00β03:00 |
| Escalation (in the paging tool) | if not acknowledged in N minutes, page the secondary, then the manager | primary asleep or out of signal |
Who watches the watcher?
If Prometheus or Alertmanager is down, nothing alerts β and silence looks exactly like health. Two standard protections:
- A dead man's switch: a rule that always fires (
expr: vector(1), often namedWatchdog), sent to an external service that pages if the heartbeat stops arriving. - Absent data alerts:
absent(up{job="order"})orabsent_over_time(...)fire when a metric disappears β a crashed exporter, a renamed metric, a job that stopped being scraped.
For journeys with little traffic at night, a ratio of zero requests is undefined; synthetic probes (a k6 or Blackbox Exporter check that buys a test ticket on a fake event every minute) keep the SLI meaningful at any hour.
Runbooks
Every page must link a runbook: a short document that tells a tired engineer what the alert means and what to try first. Runbooks reduce MTTR because the first 10 minutes stop being improvisation.
# CheckoutErrorBudgetFastBurn
**Meaning**: checkout is failing at more than 14.4x the rate the 99.9% SLO allows;
at this pace the 28-day budget is gone in about 2 days.
**Impact**: fans cannot buy tickets. During an on-sale, declare an incident immediately (SEV1).
**First checks (5 minutes)**
1. Checkout dashboard: which status codes? which route? since when? any deploy or flag change?
2. Payment provider panel: latency and error ratio of the provider call.
3. Open an exemplar trace of a failed checkout: which span fails or waits?
**Mitigations**
- Recent deploy β roll back (`argocd app rollback order`).
- Provider slow or failing β switch to the secondary provider (flag `payment.provider=secondary`).
- Database saturated β enable the waiting room (flag `waiting-room.enabled=true`).
**Escalate to**: payments team (secondary on-call), then the incident commander rotation.
Machines detect, people repair
An alert does not take a broken replica out of the load balancer: a health check does that in seconds, and wakes nobody. The page is for what a machine cannot do on its own β restore what was lost before the next failure finds BoxOffice with less redundancy than it was designed for. That is why the runbook above shortens MTTR, and why a replica that restarts and recovers by itself is not worth a page.
Try it. In observability-stack, one of the three catalog replicas dies at t = 50 s and stays dead for 80 s. Add Metrics (connected to the load balancer, the app and the database) and a Health Check connected to the load balancer, and run: errors last 3 s, and availability is 99.4%. Now add Alerting connected to Metrics and run again.
What should you have observed?
The error curve does not change: availability stays at 99.4%, because the health check had already removed the dead replica from rotation after 3 s. What changes is the repair: with someone paged, the replica is back at t = 90 s instead of t = 130 s, so BoxOffice runs on two of its three replicas for 40 s instead of 80 s. Without the health check, Alerting alone does not help the fans either β the load balancer still sends a third of the requests to the dead replica for 30 s (93.8% availability). Machines route around a failure in seconds; people repair it in minutes, and the alert is what starts the minutes.
Trade-offs
| Decision | Option A | Option B | Guidance |
|---|---|---|---|
| What to alert on | causes (CPU, restarts) | symptoms (SLO burn) | symptoms page; causes go to dashboards and tickets |
| Thresholds | static error ratio | burn rate against the SLO | burn rate β the threshold follows the business target |
| Windows | one window | long + short window | multiwindow: precise and resets quickly |
| Sensitivity | page early (more false alarms) | page late (more budget lost) | tune to the budget: fast burns page, slow burns ticket |
| Notification | everything to Slack | page / ticket / dashboard by severity | a channel everyone mutes is no alerting at all |
In the interview
When the interviewer asks "how would you know if this is broken?", answer with an SLO and its burn-rate alert, not with a list of metrics: "Checkout SLO 99.9% over 28 days; page when the 1-hour burn rate exceeds 14.4Γ confirmed by the 5-minute window, ticket for slow burns; every alert links a runbook." Mention that you alert on symptoms and use dashboards and traces for causes, and that a dead man's switch protects the alerting pipeline itself. That is a senior-level answer in four sentences.
Best practices
- Derive every page from an SLO, and keep the rules as code β the threshold then follows a business target, and every change is reviewed. In BoxOffice:
infra/grafana/alerting/slo-catalog.yaml, provisioned at startup (box above). - Exclude health checks and your own synthetic load from the SLI β orchestrators call
/actuator/healthevery few seconds and it almost never fails, inflating the success ratio with requests no fan made. In BoxOffice:http_route!~"/actuator.*"in every query of the lab 8 rules. - Put the burn rate, the windows and the runbook link in the notification β the on-call engineer judges severity before opening a laptop. In BoxOffice: the description "Burn rate 17.0 (1 h) and 200.0 (5 min)" of the lab 8 alert.
- Test every alert by causing its failure β an alert never seen firing is a hypothesis; time it from fault to page. In BoxOffice: lab 8 stops
dband records Pending and Firing (about 440 s in a test run). - Pair each ratio-based SLO alert with a "no traffic" check β with zero requests the ratio is undefined, so
noDataState: OKkeeps quiet nights quiet but also hides an edge that sends nothing; add a synthetic probe or an alert on traffic dropping to zero.
Common pitfalls
- Paging on causes: CPU, memory and restarts wake people up for nothing and train them to ignore the pager.
for: 1hto reduce noise: it also delays the page for a total outage by an hour; use burn rates instead.- One SLO window for every situation: a 28-day budget cannot protect a 30-minute on-sale; give critical events their own SLO.
- Alerts without runbooks or owners: the alert fires, nobody knows what to do, the incident lasts longer.
- No dead man's switch: the monitoring stack dies silently, and silence looks like health.
- Never deleting alerts: review every page after the shift β if it was not actionable, fix it or delete it.
How it connects
flowchart LR
SLO["SLO 99.9% over 28 days<br/>β SLIs, SLOs & error budgets"] --> BUD["error budget 0.1%"]
SLI["SLI from metrics<br/>(RED, histograms)"] --> BR["burn rate =<br/>error ratio Γ· budget"]
BUD --> BR
BR --> MW["multiwindow rules:<br/>14.4Γ and 6Γ page, 1Γ ticket"]
MW --> AM["Alertmanager: group,<br/>inhibit, silence, route"]
AM --> PAGE["page + runbook"]
AM --> TK["ticket"]
PAGE --> IR["Incident Response"]
DMS["dead man's switch,<br/>absent(), synthetic probes"] --> AM
MW -.-> ONS["on-sale SLO,<br/>short windows"] Palette components: alerting Β· metrics (the SLI and the recording rules) Β· payment (the slow provider that burns the on-sale budget) Β· kafka (a broker disk filling up: a ticket, not a page). Scenarios: observability-stack, flash-sale (a 30-minute on-sale needs its own SLO). Continues in: SLIs, SLOs & error budgets (SLI, SLO, error budget policy), Incident Response, Cloud-native β delivery (rollbacks named in the runbook), Hands-on 8 β SLO and burn-rate alert.
Key takeaways
- A page must be urgent, actionable and user-visible; everything else is a ticket, a dashboard, or nothing.
- Alert on symptoms derived from the SLO; causes are for diagnosis.
- Burn rate = observed error ratio Γ· error budget; with a 28-day window, 14.4Γ spends about 2% of the budget in an hour (14.4 Γ· 672 h).
- Use multiwindow, multi-burn-rate rules: 14.4Γ (1 h + 5 min) and 6Γ (6 h + 30 min) page, 1Γ (3 d + 6 h) opens a ticket.
- Alertmanager groups, deduplicates, inhibits, silences and routes; a dead man's switch and absent() alerts watch the watcher.
- Every alert has an owner and a runbook.
-
EWASCHUK, R. My Philosophy on Alerting. Google, 2013. Its rules are summarized in BEYER, B. et al. Site Reliability Engineering β Monitoring Distributed Systems. O'Reilly, 2016. Chapter 6.Β β©
-
BEYER, B. et al. The Site Reliability Workbook β Alerting on SLOs. O'Reilly, 2018. Chapter 5.Β β©
-
PROMETHEUS. Alertmanager and Alerting rules.Β β©