Drill — Redesign a Noisy Alert
search-api — the same service this course's SLO drill already spent thirty days measuring — has a pager rule that fires roughly 40 times a week. It's a single threshold, no burn rate, no second window, written the way most real alerting rules actually get written: someone typed in the SLO's own error allowance as a percentage and called it done. Almost none of those forty pages are worth a human's night. This drill hands you that exact rule, the last week of pages it produced, and the same 30 days of traffic data the SLO drill already gave you — and asks you to replace it with a proper four-tier multi-window burn-rate configuration, then prove, with the actual numbers, that the replacement hits a pages-per-quarter target the old rule never came close to. Work each step before you check it against the walkthrough; the arithmetic is the entire skill being drilled, not just the shape of the YAML.
Imagine an airport metal detector so sensitive it beeps at belt buckles, watches, and loose change — so often, all day, that the guard just waves everyone through without really looking anymore, because it hasn't found an actual weapon in months. That detector is worse than useless: it looks like security while training the guard to ignore it. Now imagine fixing it two ways at once. First, it only sounds the loud alarm if two different checks agree something's really there — the walk-through beep and a follow-up wand scan, not either one alone. Second, "huh, that's a little unusual" doesn't stop the whole line — it gets written down for someone to check at the end of the shift instead of holding up the airport. This drill is building that second, smarter detector for a computer system's pager.
The scenario: search-api's pager never stops ringing
☺ Like you're 10: The alarm has been going off constantly for a service that's actually fine almost every single time.
The SLO & error-budget drill already established the facts you need: search-api carries a 99.9% non-5xx SLO over a rolling 30-day window, averages about 124.86 requests/minute once health-check traffic is filtered out, and that same 30-day window contained exactly two real incidents — a bad ranking-path deploy on 2026-07-29 and a reindex job that starved a dependency on 2026-08-10. What that drill didn't show you is the alert that was supposed to catch both of those in real time. Here it is, exactly as it's been running in production for months:
groups:
- name: search-api-legacy-alerts
rules:
- alert: SearchAPIHighErrorRate
expr: |
sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="search-api",path!="/healthz"}[5m]))
> 0.001
for: 1m
labels: { severity: page }
annotations:
summary: "search-api 5xx rate above 0.1%"Notice the threshold: 0.001, or 0.1% — which is exactly 1 − SLO, search-api's own error allowance, typed in directly as a raw percentage on a single 5-minute window. This is precisely the failure mode multi-window, multi-burn-rate alerting already named: "set the threshold at the SLO's raw value and you page constantly for noise." Your first job in this drill is to prove that in numbers, not just recognize the pattern.
Reading the legacy alert: why one stray error is enough
☺ Like you're 10: At this traffic level, a five-minute window is small enough that a single mistake already looks like an emergency.
At search-api's ~124.86 req/min average, a 5-minute window carries roughly 624 requests (124.86 × 5). A single 5xx error inside that window is a ratio of 1 ÷ 624 ≈ 0.16% — already above the 0.1% threshold, on its own, with nothing else wrong. The rule doesn't detect incidents; it detects the existence of any individual failed request, wearing a percentage's clothing. Below is a representative slice of last week's PagerDuty export for SearchAPIHighErrorRate, bucketed with the same five-outcome taxonomy alert design & alert fatigue uses for its own quarterly audit — actioned, auto-resolved, duplicate, non-actionable, false positive:
| Date | Firings | Bucketed outcome | What was actually happening |
|---|---|---|---|
| 2026-08-10 | 7 | 1 actioned, 6 duplicate | The real reindex-job incident, 09:12–09:34 UTC. One page finally got investigated; the other six were the same 22-minute event re-tripping as the ratio flickered across the window's edge. |
| 2026-08-11 | 5 | 5 auto-resolved | Baseline noise — an isolated 5xx or two, cleared before the next scrape. |
| 2026-08-12 | 6 | 6 auto-resolved | Same pattern. |
| 2026-08-13 | 7 | 7 auto-resolved | Same pattern. |
| 2026-08-14 | 6 | 6 auto-resolved | Same pattern. |
| 2026-08-15 | 5 | 5 auto-resolved | Same pattern. |
| 2026-08-16 | 4 | 4 auto-resolved | Same pattern. |
| Total | 40 | 1 actioned / 6 duplicate / 33 auto-resolved | Actionability: 1 ÷ 40 = 2.5% |
2.5% actionability isn't a bad week — it's the rule working exactly as designed, and that's the trap. Alert design & alert fatigue's own worked audit found HighMemoryUsagePerPod at 2% actionable and called it a clear delete. This rule is in the same territory, except it's the only pager coverage search-api has — deleting it with nothing to replace it would trade forty noisy pages for zero real ones, the exact "coverage gap wearing a green checkmark" that page warns against.
Step 1 — re-establish the budget you're protecting
☺ Like you're 10: Before you can build a smarter alarm, you need the same numbers the old, dumb one should have been using.
Carry forward exactly what the SLO & error-budget drill already computed, because the burn-rate math in the next step is built entirely on these four numbers: SLO = 99.9%, compliance window = 30 days = 43,200 minutes, 1 − SLO = 0.001, and average valid request rate = 124.86 req/min. That drill also confirmed the window's actual spend: 7,156 total 5xx errors, 57.31 minutes of actual bad-request time against a 43.20-minute allowance — a 14.11-minute overspend, driven almost entirely by the two incidents on 07-29 and 08-10. Nothing about redesigning the alert changes any of those facts; it only changes how fast a human would have found out about them.
Step 2 — set a pages-per-quarter target before you look at the data
☺ Like you're 10: Decide how many alarms are acceptable before you build the alarm, so the numbers don't just get bent to whatever comes out.
The quarterly-audit method in alert design & alert fatigue is explicit that a floor gets set before the audit runs, precisely so the results decide the outcome instead of whoever's most attached to a rule. Do the same thing here, in the other direction: set a ceiling on legitimate pages before backtesting the new design. That same course page has already validated a real benchmark — its worked audit table found CheckoutErrorBudgetBurnFast firing 8 times in a quarter at 88% actionability and called it "keep as-is... doing exactly what it was designed to." Adopt that as search-api's target: no more than about 8 legitimate page notifications per quarter, with the expectation that a smaller, quieter service than checkout should usually land well under it.
Step 3 — derive the four burn-rate tiers
☺ Like you're 10: Same formula this course already taught, plugged in with search-api's own numbers instead of checkout's.
Every multiplier falls out of one formula from multi-window, multi-burn-rate alerting: burn_rate = consumed_% × compliance_window ÷ long_window. Because search-api shares the exact same 30-day / 43,200-minute compliance window as the checkout example that deep dive already worked through, the four resulting multipliers come out identical — not by coincidence, but because the multiplier is a property of the compliance window and the policy percentages, never of the individual service:
| Severity | Long window | Burn rate | Short window | Budget consumed if sustained | for: |
|---|---|---|---|---|---|
| Page — fast | 1 hour | 14.4× | 5 min | 2% | 2m |
| Page — moderate | 6 hours | 6× | 30 min | 5% | 2m |
| Ticket — slow | 24 hours | 3× | 2 hours | 10% | 15m |
| Ticket — slowest | 3 days | 1× | 6 hours | 10% | 1h |
The full rule set — recording rules at each of the seven windows the alerts need, then four alerts pairing them with a logical AND:
groups:
- name: search-api-slo-burn-rate
rules:
- record: sre:search_api_requests:error_ratio5m
expr: |
sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[5m]))
/
sum(rate(http_requests_total{service="search-api",path!="/healthz"}[5m]))
- record: sre:search_api_requests:error_ratio30m
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[30m])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[30m]))
- record: sre:search_api_requests:error_ratio1h
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[1h])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[1h]))
- record: sre:search_api_requests:error_ratio2h
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[2h])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[2h]))
- record: sre:search_api_requests:error_ratio6h
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[6h])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[6h]))
- record: sre:search_api_requests:error_ratio24h
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[24h])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[24h]))
- record: sre:search_api_requests:error_ratio3d
expr: sum(rate(http_requests_total{service="search-api",path!="/healthz",code=~"5.."}[3d])) / sum(rate(http_requests_total{service="search-api",path!="/healthz"}[3d]))
- alert: SearchAPIErrorBudgetBurnFast
expr: sre:search_api_requests:error_ratio1h > (14.4 * 0.001) and sre:search_api_requests:error_ratio5m > (14.4 * 0.001)
for: 2m
labels: { severity: page }
annotations:
summary: "search-api burning error budget at 14.4x — 2% of the 30-day budget in 1h"
runbook_url: "https://runbooks.acme.io/search-api-error-budget-burn-fast"
- alert: SearchAPIErrorBudgetBurnModerate
expr: sre:search_api_requests:error_ratio6h > (6 * 0.001) and sre:search_api_requests:error_ratio30m > (6 * 0.001)
for: 2m
labels: { severity: page }
annotations:
summary: "search-api burning error budget at 6x — 5% of the 30-day budget in 6h"
runbook_url: "https://runbooks.acme.io/search-api-error-budget-burn-moderate"
- alert: SearchAPIErrorBudgetBurnSlow
expr: sre:search_api_requests:error_ratio24h > (3 * 0.001) and sre:search_api_requests:error_ratio2h > (3 * 0.001)
for: 15m
labels: { severity: ticket }
annotations:
summary: "search-api burning error budget at 3x — 10% of the 30-day budget in 1d"
- alert: SearchAPIErrorBudgetBurnSlowest
expr: sre:search_api_requests:error_ratio3d > (1 * 0.001) and sre:search_api_requests:error_ratio6h > (1 * 0.001)
for: 1h
labels: { severity: ticket }
annotations:
summary: "search-api burning error budget at 1x (nominal) — 10% of the 30-day budget in 3d"Nothing in this table was chosen for search-api specifically — it's the same 14.4/6/3/1 table the checkout example already produced, because both services share the same compliance window and policy percentages. The only service-specific work left is the backtest in the next step: proving these borrowed thresholds actually fit this service's real traffic and real incident history, not just trusting the formula blindly.
Step 4 — backtest against the same 30 days
☺ Like you're 10: Replay the new alarm against last month exactly as it happened, and count how many times it would really have gone off.
This is the discipline multi-window, multi-burn-rate alerting insists on: "backtest against historical SLI data" before trusting a threshold. Reuse the exact same 30-day dataset the SLO drill handed you — the same daily request and 5xx counts, the same two incidents — and check what each of the four tiers would actually have done.
The 28 ordinary days. Daily 5xx counts across the rest of the month run 18–30 against roughly 167,000–205,000 valid requests a day, which works out to a burn rate of about 0.09× to 0.18× — comfortably under even the slowest ticket tier's 1× threshold. None of the four tiers fires on any of these 28 days. Zero pages, zero tickets, from ordinary variance.
2026-07-29 — the bad deploy. 3,850 errors landed almost entirely inside the 47-minute window between the 14:03 UTC deploy and the 14:50 UTC rollback, against a day-specific rate of ~120.63 req/min — an in-incident error ratio of 3,850 ÷ (47 × 120.63) ≈ 67.9%, a burn rate of ~679×. Assuming that rate holds roughly constant once the bad code path goes fully live (a simplification — a real rolling deploy ramps over a minute or two, which would push this slightly later), solve the window-crossing formula t = threshold × window_minutes ÷ incident_error_rate for the binding 1-hour window: t = (14.4 × 0.001 × 60) ÷ 0.679 ≈ 1.27 minutes. Add the 2-minute for: debounce and the fast tier fires at roughly T+3.3 minutes — versus the 38 minutes it actually took a human to notice via a routine dashboard check. That's about 35 minutes reclaimed, on one incident alone.
2026-08-10 — the reindex job. 2,640 errors over 22 minutes against ~119.86 req/min is a near-total outage — error ratio ≈ 100%, burn rate above 1,000×. Run the same crossing-time formula and both windows breach within roughly a minute; the fast tier fires almost as fast as Prometheus can evaluate it.
Detection speed converts directly into error budget. If the fast tier had actually paged at T+3.3 minutes on 07-29 instead of a human noticing at T+38, and the rollback still took its actual ~9 minutes once someone was on it, the incident runs about 12 minutes instead of 47 — roughly 26% of its original duration. Scale 07-29's error count by that same fraction: 3,850 × 0.2617 ≈ 1,008 errors, or 1,008 ÷ 124.86 ≈ 8.07 minutes of budget-equivalent spend, down from the 30.83 minutes it actually cost. That alone drops the month's total actual spend from 57.31 to about 34.55 minutes — flipping the SLO drill's 14.11-minute overspend into roughly an 8.65-minute surplus. This is an illustrative estimate, not a certainty — it assumes a clean, constant error rate and an unchanged mitigation time — but the direction is the entire point: catching the same bug 35 minutes sooner was worth more budget than the incident itself spent.
Step 5 — close the loop: one inhibit rule and a runbook link
☺ Like you're 10: A severe incident is loud enough to trip two alarms at once — teach them to only wake someone up once.
Run the same crossing-time math on the moderate tier (6× over a 6-hour window) for 07-29's incident and it also breaches — at roughly T+5.2 minutes, just under two minutes behind the fast tier. Left alone, a severe incident like this trips both page-severity alerts within minutes of each other, so two real incidents in a 30-day sample produce four page notifications, not two. Scaled to a quarter, that's roughly 12 notifications — already above the 8-per-quarter target from Step 2, before anything else that quarter even happens. The fix is the same structural one alert design & alert fatigue used for NodeNotReady: an inhibit rule, not a threshold change.
inhibit_rules:
# once the fast tier is already paging for this service, don't also
# let the moderate tier open a second, redundant page for it
- source_match:
alertname: SearchAPIErrorBudgetBurnFast
target_match:
service: search-api
severity: page
equal: ['service']With that one rule in place, two real incidents produce two page notifications, not four — projecting to about 6 per quarter, comfortably under the 8-per-quarter target with headroom for a slightly worse month. The last cheap fix costs nothing but a URL: every page-tier alert above carries a runbook_url annotation, following the same convention alert design & alert fatigue establishes — what the page means, the fastest safe mitigation, who to escalate to, and which dashboard confirms the fix is working. Writing that runbook for real is Capstone Part 3; this drill only requires that the link exists and points somewhere real before the rule ships.
search-api's traffic doubles overnight after a marketing push — average request rate jumps from ~124.86 req/min to roughly 250 req/min, with no change to reliability itself. Do any of the four burn-rate multipliers (14.4×, 6×, 3×, 1×) need to be recalculated for the new traffic level? Work out the reasoning before checking the checkpoint's last answer below.
Pip the Hummingbird: Forty pages this week. I'm exhausted, and I don't think I caught anything real except Wednesday's reindex mess.
Sol the Sloth: ...I ran the numbers. Your threshold is 0.1% on a five-minute window. At this traffic, one stray error already clears that. You're not paging on incidents — you're paging on arithmetic.
Foxy: So we just... raise the threshold until it stops?
Sol the Sloth: No — we stop asking "is the rate high" and start asking "is it high enough, sustained, on two different clocks at once." Fourteen-point-four times sustainable, on a five-minute window and a one-hour window, both.
Ellie the Elephant: I backtested it against the same month Sol already used for the error-budget drill. Twenty-eight boring days, zero pages. Two real incidents, two pages — well, four, until Timmy made me add the inhibit rule.
Timmy the Turtle: Four pages for two incidents projects to twelve a quarter. That's over budget before anything else even happens. One inhibit rule, and it's six.
Pip the Hummingbird: ...I could live with six a quarter. Mostly I could live with forty fewer pages a week, is what I actually mean.
1. Using search-api's ~124.86 req/min average, show why the legacy alert's 0.1% threshold on a 5-minute window is guaranteed to fire on almost any single 5xx error — and name the specific anti-pattern this is from the burn-rate deep dive. 2. What two inputs does the burn-rate multiplier formula need, and why did search-api's four tiers come out numerically identical to checkout-api's, with no new derivation? 3. Roughly how much sooner would the redesigned fast tier have caught the 07-29 deploy than the human who noticed it via a routine dashboard check, and what did that do to the day's contribution to the 30-day error budget? 4. Concretely, with this drill's own numbers, why does one inhibit_rule matter for hitting the 8-pages-per-quarter target? 5. True or false: doubling search-api's traffic would require recalculating the four burn-rate multipliers. Why or why not?
Check your answers
- A 5-minute window at ~124.86 req/min carries roughly 624 requests (124.86 × 5); a single 5xx error is already a ratio of 1 ÷ 624 ≈ 0.16%, which exceeds the 0.1% threshold on its own. This is the exact anti-pattern multi-window, multi-burn-rate alerting names as "alert on a short window's raw rate" set "at the SLO's own value" — it buys detection speed and gives up nearly all precision.
burn_rate = consumed_% × compliance_window ÷ long_window— it needs the percentage of budget you're willing to let burn before alerting, and the long window's length as a fraction of the compliance window. Both search-api and checkout-api share the same 30-day / 43,200-minute compliance window and the same four policy percentages (2%/5%/10%/10%), so the formula produces identical multipliers for both — the multiplier is a property of the window and the policy, not of the individual service.- The 1-hour window crosses 14.4× at roughly t = (14.4 × 0.001 × 60) ÷ 0.679 ≈ 1.27 minutes after the incident's error rate takes hold; with the 2-minute
for:debounce, the alert fires around T+3.3 minutes — about 35 minutes sooner than the human's 38-minute manual detection. Assuming the same ~9-minute mitigation time, the incident runs ~12 minutes instead of 47, cutting its budget contribution from ~30.83 minutes to ~8.07 minutes — enough on its own to flip the month's overall 14.11-minute overspend into roughly an 8.65-minute surplus. - Without inhibition, a severe incident crosses both the fast and moderate thresholds within a couple of minutes of each other, so one real incident produces two page notifications. Two such incidents in a 30-day sample then project to about 12 notifications per quarter — already over the 8-per-quarter target before anything else happens. One inhibit_rule that suppresses the moderate page whenever the fast tier is already open for the same service collapses that to one notification per incident, projecting to about 6 per quarter — comfortably under target.
- False. Burn rate is an observed error ratio divided by (1 − SLO) — a ratio of ratios — so it's already normalized against traffic volume. Doubling the request rate doubles the requests and, proportionally, the errors behind any given percentage, leaving the ratio and its burn rate unchanged. What changes is the absolute blast radius: the same percentage now represents twice as many real failed requests, even though the alert fires at exactly the same burn-rate multiplier and exactly the same relative time.
One legacy rule, one afternoon of arithmetic, four tiers, one inhibit rule — and a pager that goes from 40 firings a week to roughly 6 a quarter without losing a single real incident. If any step here felt shaky, the full treatment is one click away: multi-window, multi-burn-rate alerting for the windowing math, alert design & alert fatigue for the audit taxonomy and the inhibition mechanics, and the SLO & error-budget drill for where this same month's numbers first came from. The incident-response tabletop picks up the moment one of these redesigned alerts actually fires for real, and Capstone Part 2 — build the monitoring & alerting is this exact exercise again, on a service and a dataset you build yourself instead of one handed to you.