Search Tech Journey

Find topics, journeys and posts

6-month learning plan79 / 130
back to blog
systemsintermediate 55m read

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.

⚙️SystemsM09 · Observability & SRE· Session 079 of 130 90 min

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

You will be able to
  • 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's health inspection score
🌍 Real world

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.

💻 Code world

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

Learn these definitions verbatim
  • 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

  1. 90%
    36.5 days/year down
    Not really a service. A weekend hobby project.
  2. 99%
    3.65 days/year, 7.2 hours/month
    Achievable with one on-call rotation, minimal HA.
  3. 99.9% ('three nines')
    8.76 hours/year, 43 min/month
    Real HA (multi-AZ), automated runbooks, tested deploys. Common for SaaS.
  4. 99.95%
    4.4 hours/year, 22 min/month
    AWS-tier managed services. Requires disciplined ops.
  5. 99.99% ('four nines')
    52.6 min/year, 4.4 min/month
    Multi-region, chaos-tested, expensive. Only for critical infra (auth, payments).
  6. 99.999% ('five nines')
    5.26 min/year, 26 sec/month
    Extremely 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

11
Availability (request-based)

good / total. Where 'good' = HTTP status <500 or successful business result. Most common SLI.

22
Latency

fraction of requests below a latency threshold. E.g. '99% of requests under 200ms'.

33
Freshness

how stale is data allowed to be? 'X% of dashboard reads see data <60s old'. Relevant for batch / streaming.

44
Correctness

% 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'

Threshold alerts (old school)
Alert when latency > 500ms for 5 min. Fires on transient spikes; doesn't relate to user impact.
noisy
Symptom alerts
Alert on things users would notice: elevated error rate, high latency at percentiles, saturation. Better but still noisy.
better
Error-budget alerts (single burn rate)
Alert when you're burning budget faster than a threshold sustained over a window. Fewer false positives.
SLO
Multi-window multi-burn-rate
Two alerts: fast burn (2% of budget in 1 hour = severe) + slow burn (10% in 6 hours = investigate). Google's SRE book recommends this combo.
state of the art

Comparing common SLO choices

99% (2-nines)

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
99.9% (3-nines)

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
99.99% (4-nines)

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


Common misconception
✗ What most people think

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

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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.

From first principles
Start with the question

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.

  1. 1
    A 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
  2. 2
    Alerting 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
  3. 3
    Fixing 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
  4. 4
    So 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%
  5. 5
    Still, 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

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.

Mental modelThe budget is the currency between reliability and velocity

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.
🔔 Fires when you see

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

The tradeoff

Where do you measure the SLI — server-side metrics, synthetic probes, or real-user/client telemetry?

Server-side request metrics
+ you gain free (you already emit them), complete coverage of every request, exact attribution to a service and endpoint, and the lowest cardinality of the three
− you pay structurally blind to everything between your load balancer and the user: DNS, TLS negotiation, CDN faults, client network, and — critically — requests that never arrived. A total ingress outage can look like a beautiful 100% success rate on zero traffic.
pick when the default SLI for internal services and APIs whose callers are other services inside your own network
Synthetic probes (blackbox)
+ you gain gives you a signal at constant rate even at 3am with no traffic, so "no data" is distinguishable from "no users"; runs from outside your network, so it sees the whole path
− you pay it measures a handful of synthetic journeys, not what users actually do; it will happily report green while a code path taken by 40% of real traffic is broken. Probe frequency also bounds your resolution — a 60s probe cannot detect a 20s outage reliably.
pick when you need coverage during low-traffic hours, or the service is user-facing and the network path itself is part of what you are promising
Real-user / client-side telemetry
+ you gain the only measurement that reflects what a user experienced, including client render time, retries, and failures that never reached your servers — the closest proxy for "was the user happy"
− you pay the reporting channel shares fate with the thing being measured (a client that cannot reach you cannot report that it cannot reach you); it is high-cardinality and expensive; and it is subject to sampling, ad-blockers, and privacy constraints
pick when the SLO is about a human-facing experience — page load, app startup, first meaningful interaction — where server timing is a poor proxy
What a senior engineer actually does

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

SLOConfig with burn multipliers
14.4x and 6x aren't magic — they come from Google's SRE workbook. 14.4x over 1h consumes 2% of a 30-day budget; 6x over 6h consumes 10%. These are the recommended thresholds.
config
SLOTracker.observe · rolling deques
Three windows: full 30d for SLI, 1h for fast burn, 6h for slow burn. Deques auto-prune. In production Prometheus does this with recording rules.
windows
error_budget_remaining
The KEY number for policy. If it's > 50% you're healthy, ship freely. If < 10%, freeze non-essential deploys. If 0, all-hands reliability.
policy
burn_rate
actual_bad / allowed_bad. Nominal 1x = burning exactly at the target rate. 14x means you'll exhaust the monthly budget in 2 days at this rate.
math
multi-window alerts
PAGE for fast (major regression, wake someone up) + TICKET for slow (investigate but not urgent). Reduces pager fatigue while catching both classes of incident.
alerting
Try itDesign an SLO and error budget policy for a real service

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.

💡 Hint · Pick a service (checkout, search, feed). List 2-3 SLIs. For each, pick an SLO target with justification. Compute the monthly error budget. Write a one-paragraph 'error budget policy' — what happens when budget is >50%, 10-50%, and <10%. This is what SRE teams call an EBP (Error Budget Policy).

(d) Production reality · 15 min

War story Google · birth of SREthe entire discipline
🔥 What broke

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

🧯 The fix
Ben Treynor Sloss's team invented Site Reliability Engineering. Central innovation: pick an explicit SLO. Compute the error budget. Both product and SRE now speak the same language. If the budget is fat, ship faster (SRE encourages it). If it's exhausted, freeze deploys until it recovers. Zero politics — the math decides.
🎓 Lesson to steal
SLOs are not just measurement — they're a POLITICAL protocol. Error budgets align Product and SRE incentives so neither side has to advocate for the other's concerns. This is the biggest reason SRE succeeded where 'devops' as a philosophy struggled.
Post-mortem
War story Common failure — 'reliability theatre'Documented at hundreds of companies
🔥 What broke
Team declares SLOs of 99.99% for every service, celebrates hitting them each month. Reality: their measurement excludes 'planned' outages, 'expected' errors, third-party dependency failures, and anything the team decided didn't count. Users see much worse — they don't care about your exclusions.
🧯 The fix
SLIs should be measured from the USER'S perspective, not the service's. Best-practice SLI: 'proportion of homepage loads that succeeded AND rendered in less than 2s' — as seen from the browser (via RUM data or synthetic monitoring), not from server logs. Include third-party dependencies your users can't distinguish from you.
🎓 Lesson to steal
An SLO that doesn't reflect user pain is worse than no SLO — it manufactures false confidence. Ask: 'if my SLO is met but users are complaining, is my SLO wrong?' Yes. Redefine.
War story NetflixChaos engineering + SLOs
🔥 What broke
Netflix's playback team wanted to push weekly deploys but was blocked by 'stability week' every quarter. Deploys accumulated in massive risky releases.
🧯 The fix
Adopt explicit SLOs on playback start success rate. Chaos Monkey (deliberately killing servers) is only allowed to run while error budget is healthy — it CONSUMES budget the same way real incidents do. When budget is low, chaos stops until it recovers. Now deploys are continuous; chaos is scheduled science, not a threat.
🎓 Lesson to steal
Error budgets can pay for chaos experiments, canaries, load tests. Anything that risks user impact should have a 'budget cost' calculated. Makes the trade-off explicit and defensible.
Post-mortem

Where this shows up in the rest of the plan

SLOs are the language of reliability at every senior level
S077 · 3 pillars
SLIs are the metrics from the metrics pillar.
S078 · Prometheus / Grafana
Where you actually implement SLI measurement + burn-rate alerts.
S080 · Incident response
SLO burn is the definition of 'incident'. Error budget is what determines severity.
S076 · Multi-region
SLO decides whether you need multi-region at all. 99.9% doesn't; 99.99% does.
S089 · Rate limiting
Rate limits protect SLO. Reject early when saturated rather than degrade for everyone.
S128 · System design interviews
Every senior design answer includes 'the SLO would be...'. Sr+ interviewers watch for this.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

Teach these three, no notes:

  1. SLI vs SLO vs SLA — one sentence each, with the ordering.
  2. The error budget concept — why it aligns Product and SRE.
  3. 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.