S079 · SLIs, SLOs & Error Budgets — the SRE Math
How to measure reliability without lying to yourself. The formulas Google's SRE team uses to decide when to ship features vs when to freeze deploys.
🎯 Define SLIs and SLOs for a real service, compute the error budget for a target of 99.9% availability, and design a burn-rate alert.
Why this session exists
Every team has been in this meeting: "Are we reliable?" "Yes." "How do you know?" "Uh, dashboards look green?" That is not an answer, and it's the reason "reliability" is a political football at most companies. Google's SRE book codified the math: pick specific numbers (SLIs) that measure user pain, set a target (SLO), compute what remains (error budget), and use the budget as a policy lever. If you're under budget, ship faster. If you're over, freeze and fix. This session teaches you the vocabulary and the arithmetic — you'll never have that meeting again.
- Define SLI, SLO, SLA, and error budget in one sentence each and explain how they relate.
- Pick appropriate SLIs for a web API, a batch job, and a message queue.
- Compute the minutes of downtime allowed by 99%, 99.9%, 99.99% SLOs — from memory.
- Design a burn-rate alert that catches fast burns without paging on every blip.
- Explain why 100% reliability is the wrong target for any product that ships.
Prerequisites
- S077 · 3 pillars of observability — SLIs are metrics.
- S078 · Prometheus / Grafana — you'll write SLO alerts in PromQL.
(a) Intuition · 5 min
A restaurant doesn't promise 'zero food safety issues, ever' — that would require infinite spending and still not be achievable. Instead the health department picks specific measurable things (temperature of the fridges, hand-washing frequency, expiration dates) and requires each to be above a threshold. If the restaurant falls below on any one, they lose points. Below 90 points, the inspector comes back in a week. Below 80, the restaurant closes until fixed.
SLIs are the specific measurable things (request success ratio, p99 latency, queue lag). SLOs are the thresholds (99.9% of requests succeed, p99 < 300ms, lag < 60s). Error budget is 'how far below can you go before shipping stops and firefighting starts'. Just like a restaurant, 100% is not the target — it's economically irrational and unachievable.
The genius: error budget aligns incentives. Product wants to ship; SRE wants stability. Error budget makes both sides use the same currency. If the budget is fat, ship. If it's exhausted, freeze.
The four terms every senior engineer must define cleanly
- SLI (Service Level Indicator) — a MEASUREMENT of some user-facing aspect of reliability, expressed as a ratio 0-1. Example: 'proportion of HTTP requests that returned <500 status'.
- SLO (Service Level Objective) — a TARGET for an SLI, chosen by product + engineering, expressed as a threshold + window. Example: '99.9% of HTTP requests over a rolling 30 days'.
- Error budget — the ALLOWED FAILURE. If SLO is 99.9%, error budget = 0.1% = 43 min/month of full outage, or equivalent partial degradation.
- SLA (Service Level Agreement) — a CONTRACT with an external customer. If violated, money changes hands (credits, refunds). SLA target is always looser than SLO — the internal SLO is the 'do not cross' line, the SLA the 'you get money back' line.
The magic downtime table — memorise this
- 90%36.5 days/year downNot really a service. A weekend hobby project.
- 99%3.65 days/year, 7.2 hours/monthAchievable with one on-call rotation, minimal HA.
- 99.9% ('three nines')8.76 hours/year, 43 min/monthReal HA (multi-AZ), automated runbooks, tested deploys. Common for SaaS.
- 99.95%4.4 hours/year, 22 min/monthAWS-tier managed services. Requires disciplined ops.
- 99.99% ('four nines')52.6 min/year, 4.4 min/monthMulti-region, chaos-tested, expensive. Only for critical infra (auth, payments).
- 99.999% ('five nines')5.26 min/year, 26 sec/monthExtremely rare. Global telecom, air-traffic control. Sub-second failover, redundant everything.
(b) Visual walkthrough · 15 min
The SLI → SLO → error budget flow
Error-budget burn over 30 days
The four types of SLI
good / total. Where 'good' = HTTP status <500 or successful business result. Most common SLI.
fraction of requests below a latency threshold. E.g. '99% of requests under 200ms'.
how stale is data allowed to be? 'X% of dashboard reads see data <60s old'. Relevant for batch / streaming.
% of outputs verified against a reference. Hardest to measure. Used in payments, ML pipelines.
The two alerting philosophies
From 'wake up every night' to 'wake up for real problems'
Comparing common SLO choices
Internal tools, alpha features
- 3.65 days/year down = a very long weekend
- Cheap to hit: single region, one on-call
- Users tolerate: internal dashboards, batch reports
- Bad for anything customer-facing
Default for SaaS
- 43 min/month = one big incident/quarter
- Multi-AZ, HA database, deploy discipline
- Sensible SLA offer: 99% (with buffer)
- The economically defensible sweet spot
Auth, payments, tier-0 infra
- 4.4 min/month = a coffee break
- Multi-region, chaos-engineering, gamedays
- Costs 3-5x more than 99.9% infra + team
- Justified only when downtime = lawsuits/lost trust
The mental model to hold
"Our target is 99.99% availability. Anything less than that is a failure, so the goal every quarter is to get as close to 100% as we can."
An SLO is a budget, not a floor. 99.9% availability explicitly grants you ~43 minutes of unavailability per 30 days. Spending zero of that budget is not excellence — it is evidence that you over-provisioned, under-shipped, or set the target above what users actually need. The correct steady state is spending most of the budget most of the time.
The myth is sticky because every other number in engineering is a floor: test coverage, uptime in a contract, p99 in a customer promise. And it is genuinely true for the contractual SLA, where breaching costs money. But the SLA and the SLO are different instruments: the SLA is what you promise externally with penalties, the SLO is the internal control loop you set tighter than the SLA so you get warning before the penalty. Confusing the control loop for the promise turns a risk-management tool into a shame metric — and the predictable result is that teams stop shipping, or start hiding incidents.
Convert your target to minutes and see how much you are actually allowed to burn. Then compute your remaining budget as a query rather than arguing about it:
# error budget remaining over a 30d window, as a fraction of budget
# SLO = 99.9% -> allowed bad fraction = 0.001
1 - (
(1 - (sum(rate(http_requests_ok_total[30d]))
/ sum(rate(http_requests_total[30d]))))
/ 0.001
)If that number sits near 1.0 quarter after quarter, your SLO is too loose, not your service too good.
Why is multi-window, multi-burn-rate alerting the standard, instead of the obvious "alert when error rate exceeds the SLO threshold"? The simple rule looks correct — it isn't.
- 1A useful alert must have both high recall (it fires for outages that will actually exhaust the budget) and high precision (nearly every page corresponds to something worth waking a human for).forced by · a low-precision pager is ignored within two weeks, which drops effective recall to zero regardless of the math
- 2Alerting on "instantaneous error rate above the SLO threshold" fails precision catastrophically. At a 99.9% SLO, a single 30-second blip pushes the short-window rate above 0.1% and pages you — even though it consumed a fraction of a percent of a 30-day budget.forced by · the threshold is defined over a 30-day window but you evaluated it over seconds
- 3Fixing that by evaluating over the full 30-day window destroys recall in the other direction: a total outage would need hours before the 30-day average crosses the line. You get paged long after the budget is gone.forced by · a long averaging window has detection latency proportional to that window
- 4So the quantity to alert on is not error rate but burn rate: error rate divided by the budgeted rate. Burn rate 1 means you exhaust the budget exactly at the end of the window; burn rate 14.4 means you exhaust it in ~2 days. This makes severity independent of the SLO number itself.forced by · you need one dimensionless quantity that means the same thing at 99.9% and 99.99%
- 5Still, a single burn-rate window is either twitchy (short) or slow (long). Requiring a fast window and a slow window to both be over threshold gives fast detection from the short window and blip-immunity from the long one — and the short window doubles as an automatic reset, so the alert clears quickly once the burn stops.forced by · an AND of a sensitive and a stable detector inherits the latency of the fast one and the precision of the slow one
Therefore the canonical setup is tiered: a high burn rate over a short pair of windows pages immediately, a low burn rate over a long pair of windows opens a ticket. Severity is derived from how fast the budget is disappearing, not from how bad the error rate looks in isolation.
And note what this predicts: the same construction must apply to latency and correctness SLOs, not just availability — because nothing in the derivation mentioned errors. Any SLI expressible as good-events/total-events gets identical burn-rate alerting for free. That is why "define the SLI as a ratio of good to valid events" is the first rule, not an afterthought.
Picture a single shared account, refilled on a rolling window. Every bad request debits it. Shipping risky changes debits it faster. The account balance is the only thing both the SRE and the product manager are allowed to argue with, and it converts an unwinnable values debate ("ship faster" vs "be more stable") into arithmetic.
Budget remaining ⇒ you have earned the right to take risk: ship, experiment, do the risky migration. Budget exhausted ⇒ the policy, agreed in advance and in writing, freezes risky changes until it refills. Nobody negotiates in the middle of an incident because the rule was signed before the incident.
- SLI = good events / valid events, measured as close to the user as you can get. If it is measured server-side only, you are blind to exactly the failures users see most.
- SLO = the target on that ratio over a stated window. No window ⇒ not an SLO.
- Error budget = 1 − SLO, expressed in events or minutes. Burn rate = actual bad-rate ÷ budgeted bad-rate.
- SLA ⊃ SLO: always set the internal SLO tighter than the external SLA, so you get a warning shot before a penalty.
Fire this the moment you see: an alert threshold picked because "that number felt right" · a 100% uptime goal · a reliability-vs-velocity argument with no shared number · a p99 SLO with no stated window · an SLI computed only from server-side logs · a postmortem action item of "be more careful".
Where do you measure the SLI — server-side metrics, synthetic probes, or real-user/client telemetry?
Use server-side as the primary SLI because it is complete and cheap, and pair it with a synthetic probe whose only job is to make "zero traffic" distinguishable from "zero errors". That pairing removes the single most dangerous failure mode of server-side SLIs — looking healthy while dark.
Add client telemetry only for the small number of SLOs that are genuinely about human experience, and accept up front that it is sampled and lossy. The senior move is not picking one; it is knowing which blind spot each one has and covering that specific blind spot with exactly one other signal, rather than running three dashboards nobody reconciles.
(c) Hands-on · 25 min
Let's build a Python simulator that emits fake service traffic, computes SLI/SLO/error budget in real time, and fires multi-burn-rate alerts. All the SRE math in one runnable file.
#!/usr/bin/env python3
"""slo_simulator.py — walk through the SLO math with live traffic.
Run: python slo_simulator.py
Simulates:
- a service serving 100 rps with 0.05% baseline error rate
- an incident partway through that spikes error rate to 5%
- real-time SLI computation over a 30-day-equivalent window
- error budget consumption
- multi-burn-rate alerts (fast + slow)
"""
from __future__ import annotations
import random
import time
from collections import deque
from dataclasses import dataclass
@dataclass
class SLOConfig:
target: float = 0.999 # 99.9%
window_seconds: int = 30 * 24 * 3600 # 30 days
# Fast burn: consume 2% of budget in 1 hour = 14.4x normal burn rate
fast_burn_multiplier: float = 14.4
fast_burn_window: int = 3600 # 1 hour
# Slow burn: consume 10% of budget in 6 hours = 6x normal
slow_burn_multiplier: float = 6.0
slow_burn_window: int = 6 * 3600
class SLOTracker:
def __init__(self, cfg: SLOConfig):
self.cfg = cfg
# Retain a rolling window of (timestamp, is_good) samples
self.events: deque[tuple[float, bool]] = deque()
# Rolling counts for fast/slow burn windows
self.recent_1h: deque[tuple[float, bool]] = deque()
self.recent_6h: deque[tuple[float, bool]] = deque()
def observe(self, is_good: bool, now: float | None = None) -> None:
now = now or time.time()
self.events.append((now, is_good))
self.recent_1h.append((now, is_good))
self.recent_6h.append((now, is_good))
# Prune each window
cutoff = now - self.cfg.window_seconds
while self.events and self.events[0][0] < cutoff:
self.events.popleft()
while self.recent_1h and self.recent_1h[0][0] < now - self.cfg.fast_burn_window:
self.recent_1h.popleft()
while self.recent_6h and self.recent_6h[0][0] < now - self.cfg.slow_burn_window:
self.recent_6h.popleft()
@staticmethod
def _bad_ratio(events: deque[tuple[float, bool]]) -> float:
if not events:
return 0.0
bad = sum(1 for _, g in events if not g)
return bad / len(events)
def sli(self) -> float:
return 1.0 - self._bad_ratio(self.events)
def error_budget_remaining(self) -> float:
"""Fraction of budget still available (0-1)."""
allowed_bad = 1.0 - self.cfg.target
actual_bad = self._bad_ratio(self.events)
if allowed_bad == 0:
return 0.0
remaining = 1.0 - (actual_bad / allowed_bad)
return max(0.0, remaining)
def burn_rate(self, window_events: deque[tuple[float, bool]]) -> float:
"""How fast are we consuming budget, expressed as multiplier of nominal.
nominal = SLO_error_ratio (e.g. 0.001 for 99.9%)."""
allowed_bad = 1.0 - self.cfg.target
actual_bad = self._bad_ratio(window_events)
if allowed_bad == 0:
return float("inf") if actual_bad > 0 else 0.0
return actual_bad / allowed_bad
def alerts(self) -> list[str]:
alerts = []
fast = self.burn_rate(self.recent_1h)
slow = self.burn_rate(self.recent_6h)
if fast >= self.cfg.fast_burn_multiplier:
alerts.append(f"PAGE · fast burn {fast:.1f}x over 1h "
f"(threshold {self.cfg.fast_burn_multiplier}x)")
if slow >= self.cfg.slow_burn_multiplier:
alerts.append(f"TICKET · slow burn {slow:.1f}x over 6h "
f"(threshold {self.cfg.slow_burn_multiplier}x)")
return alerts
def simulate() -> None:
cfg = SLOConfig(target=0.999)
tracker = SLOTracker(cfg)
print(f"SLO target: {cfg.target*100:.2f}%")
print(f"Error budget: {(1-cfg.target)*100:.3f}% "
f"= {(1-cfg.target) * 30 * 24 * 60:.1f} min/month full downtime")
print(f"Fast burn threshold: {cfg.fast_burn_multiplier}x over 1h")
print(f"Slow burn threshold: {cfg.slow_burn_multiplier}x over 6h\n")
# Simulate 8 hours of traffic. Fast-forward: 1 real second = 60 sim minutes.
RPS = 100
SPEEDUP = 3600 # 1 real sec = 1 sim hour
start_real = time.time()
now_sim = start_real # timestamps in sim time
incident_start = start_real + 3.0 # sim: hour 3
incident_end = start_real + 3.5 # sim: hour 3.5
for hour in range(8):
# Serve one hour of RPS traffic in a burst
error_rate = 0.0005 # baseline 0.05%
real_now = time.time()
if incident_start <= real_now <= incident_end:
error_rate = 0.05 # 5% during incident
print(f"[hour {hour}] INCIDENT — error rate 5%")
for _ in range(RPS * 3600): # requests in this hour
is_good = random.random() > error_rate
tracker.observe(is_good, now=now_sim)
now_sim += 1.0 / RPS
sli = tracker.sli() * 100
budget = tracker.error_budget_remaining() * 100
alerts = tracker.alerts()
alert_str = " | ".join(alerts) if alerts else "healthy"
print(f"[hour {hour}] SLI={sli:.3f}% budget_remaining={budget:5.1f}% "
f"{alert_str}")
# Advance real time to simulate an hour
time.sleep(1.0)
print("\nSimulation complete.")
print(f"Final SLI: {tracker.sli()*100:.3f}%")
print(f"Final budget remaining: {tracker.error_budget_remaining()*100:.1f}%")
if __name__ == "__main__":
simulate()What each block does
Anatomy of the simulator
Template to fill in:
Service: [checkout API]
SLIs:
- Availability: requests returning non-5xx / total requests
- Latency: requests with p95 < 300ms / total requests
SLOs (rolling 30 days):
- Availability: 99.9% (43 min/month allowed error)
- Latency: 99% of requests under 300ms
Error Budget Policy:
- > 50% budget remaining: normal cadence, ship anything
- 10-50%: canary all changes, no experimental features
- < 10%: deploy freeze except for reliability fixes; SRE lead escalation
- 0%: incident review + retro, SLO renegotiation if unrealistic
Commit this to your team's runbook repo. Review quarterly.
(d) Production reality · 15 min
In the early 2000s Google's engineering teams wanted to ship features; the ops teams wanted stability. Constant fights. Every release week was a war. Some teams over-invested in reliability (99.999% pursued when 99.9% would have sufficed), others under-invested (product broke constantly).
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
Teach these three, no notes:
- SLI vs SLO vs SLA — one sentence each, with the ordering.
- The error budget concept — why it aligns Product and SRE.
- Why 100% reliability is the wrong target — the economic argument.
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.