Skip to content

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:

\[ \text{burn rate} = \frac{\text{error ratio observed over a window}}{1 - \text{SLO}} \]

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

\[ \text{budget consumed} = \frac{b \cdot t}{\text{SLO window}} \]
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%).

2026-11-01T21:54:31.222556 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ 1 0 0 1 0 1 1 0 2 error ratio of the incident (%) 1 0 0 1 0 1 1 0 2 time to fire (minutes) SLO 99.9% β€” how fast the burn-rate alerts fire page: 14.4Γ— over 1 h (+5 min) page: 6Γ— over 6 h (+30 min)

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).

infra/grafana/alerting/slo-catalog.yaml (the fast-burn page)
- 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 named Watchdog), sent to an external service that pages if the heartbeat stops arriving.
  • Absent data alerts: absent(up{job="order"}) or absent_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/health every 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 db and 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: OK keeps 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: 1h to 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.


  1. 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. ↩

  2. BEYER, B. et al. The Site Reliability Workbook β€” Alerting on SLOs. O'Reilly, 2018. Chapter 5. ↩

  3. PROMETHEUS. Alertmanager and Alerting rules. ↩