Reliability Engineering
How SREs turn “it should be up” into numbers you can defend: measure the right signals, set targets you can honour, spend an error budget deliberately, and protect the tail with timeouts, backoff and circuit breakers. This is the vocabulary that separates “I hope it works” from “here is my SLO and here is how I know when to stop shipping features.”
Reliability is not a feeling — it is a measured ratio compared against a target, with a budget for how much you are allowed to fail. Everything in this chapter hangs off four nested ideas: you measure a signal (SLI), you target it internally (SLO), you promise a looser version externally (SLA), and the gap below perfection is your error budget — the fuel you spend on shipping.
SLI vs SLO vs SLA vs Error Budget
These four terms get muddled in interviews. Keep them crisp: an SLI is what you measure, an SLO is what you aim for, an SLA is what you contractually promise, and the error budget is the slack between your SLO and 100% that you are permitted to burn.
SLI — Service Level Indicator
A measured signal about behaviour, expressed as a ratio of good events over valid events. E.g. success_rate = 2xx+3xx / all, or “fraction of requests faster than 200 ms”. It is a number you can graph, not an opinion.
SLO — Service Level Objective
An internal target on an SLI over a window: “99.9% of requests succeed, measured over 28 days”. It is stricter than the SLA and is the line your alerts and release policy are built around.
SLA — Service Level Agreement
An external contract with a customer, carrying penalties (refunds, credits) if breached. Always set looser than the SLO so you trip your own alarms and fix things before a customer can claim.
Error budget
budget = 1 − SLO. At 99.9% monthly you are allowed ~0.1% failure ≈ 43.2 min/month of badness. It is a resource: spend it on risky launches, or conserve it when you are close to breaching.
The measured SLI should sit above the internal SLO target, which sits above the external SLA floor. Order of strictness: SLA < SLO ≤ SLI (hopefully). If your SLA equals your SLO you have no safety margin — the customer finds out at the same moment you do.
Error‑budget math (the number to memorise)
Pick a window (28 or 30 days is common). The budget is the complement of the SLO applied to that window:
# 99.9% monthly SLO, 30-day window
budget_fraction = 1 - 0.999 # = 0.001 (0.1%)
month_minutes = 30 * 24 * 60 # = 43,200 min
budget_minutes = 0.001 * 43200 # ≈ 43.2 min/month of allowed badness
“We treat the budget as a shared currency. While it’s healthy, product can ship aggressively. When burn accelerates or the budget is exhausted, we invoke the policy: freeze risky feature launches, and redirect the team to reliability work until we’re back in budget. It converts an argument (‘is it reliable enough?’) into a rule.”
Burn‑rate alerting: don’t alert on the raw SLI
Alerting the instant the SLI dips is noisy. Instead alert on burn rate — how fast you are consuming the budget relative to the window. A burn rate of 1× exhausts the whole month’s budget in exactly a month; 14.4× burns it in ~50 hours. Use multi‑window, multi‑burn‑rate alerts: a fast‑burn window catches acute outages quickly, a slow‑burn window catches quiet chronic erosion without paging at 3 a.m. for a blip.
| Alert class | Budget consumed | Long / short window | Burn rate | Action |
|---|---|---|---|---|
| Fast‑burn | 2% of monthly budget | 1 h & 5 min | 14.4× | Page on‑call now |
| Medium‑burn | 5% of monthly budget | 6 h & 30 min | 6× | Page (business hours OK) |
| Slow‑burn | 10% of monthly budget | 3 d & 6 h | 1× | Open a ticket |
The short window is a reset guard: both the long and short window must be burning for the alert to fire, so it clears quickly once the incident is mitigated instead of ringing for another hour.
The availability “nines”
“How many nines?” is shorthand for how much downtime you tolerate. Memorise the top rows — three and four nines are what most interview systems target.
| Availability | Nickname | Downtime / year | Downtime / month | Downtime / day |
|---|---|---|---|---|
| 90% | one nine | 36.5 days | 72 hours | 2.4 hours |
| 99% | two nines | 3.65 days | 7.2 hours | 14.4 min |
| 99.9% | three nines | 8.76 hours | 43.2 min | 1.44 min |
| 99.95% | 4.38 hours | 21.6 min | 43.2 s | |
| 99.99% | four nines | 52.6 min | 4.32 min | 8.64 s |
| 99.999% | five nines | 5.26 min | 25.9 s | 0.86 s |
If a request must pass through n independent components in series, availability is the product: A = A₁ · A₂ · … · Aₙ. Ten hops at 99.9% each give 0.999¹⁰ ≈ 99.0% — you dropped two whole nines just by adding hops. This is why you minimise the critical path, make dependencies parallel where possible, and use fallbacks/caches so a dependency’s failure doesn’t automatically fail the request.
Availability Zones & Regions — where the redundancy lives
Those nines don’t come from one very reliable machine — they come from redundancy across independent failure domains. Cloud providers hand you a ready‑made hierarchy of them: a Region contains several Availability Zones, and you place copies of your system across a few so that losing one doesn’t take you down.
Availability Zone (AZ)
One (or a few) physically isolated data centres with independent power, cooling and networking. A fire, flood, or power cut in one AZ is designed not to affect the others. Named like us‑east‑1a, ‑1b, ‑1c.
Region
A geographic area (e.g. us‑east‑1, eu‑west‑1) that contains multiple AZs — usually 3+. The AZs sit a few ms apart on private links, close enough to replicate synchronously between them.
Multi‑region
Copies in different geographies, tens–hundreds of ms apart. Buys survival of a whole‑region outage plus lower latency for distant users — but the distance forces async replication and eventual consistency.
Rule of thumb: spread every stateless tier and your database across ≥ 2–3 AZs in one region for everyday high availability — a whole AZ can drop and you just lose a slice of capacity, not the service. Add multi‑region only when you need disaster recovery or global latency, because it’s the expensive, complicated tier.
Cross‑AZ = same region, ~1–2 ms apart → cheap, synchronous replication, so a failover loses no data. Cross‑region = far apart, tens–hundreds of ms → asynchronous only, so a failover can lose the last few seconds of writes — but it survives an entire region going down. Most systems are multi‑AZ by default, and go multi‑region only when the SLO (or a data‑residency law) demands it.
Latency percentiles — why the mean lies
Latency distributions are right‑skewed: most requests are fast, but a long tail of slow ones drags the average to the right of the typical experience. A mean of 55 ms can hide a p99 of 250 ms. Report percentiles, not averages: pN is the value below which N% of requests fall.
Forget averages for a second. Imagine 100 people just used your app. Write down how long each one waited, then line them up from fastest to slowest and number them 1 to 100. A percentile just points at one person in that line:
- p50 = person #50 — the middle of the line. Half of your users were faster, half were slower. This is the typical experience (its other name is the median).
- p90 = person #95 — 95 out of 100 waited this long or less. Only the 5 slowest people had it worse.
- p99 = person #99 — 99 out of 100 were this fast or faster. Only the single unluckiest person waited longer.
So “p99 = 250 ms” simply means “99 out of every 100 requests finished within 250 ms.” You never add or divide anything — you just sort the times and read off one value. The bigger the number after the p, the deeper into the rare, slow tail you are looking, and the worse (but less common) the experience it describes.
p50 (median)
The typical request. Good for a sanity check, useless for tail‑sensitive UX. Half your users are slower than this.
p95 / p90
Where the pain starts. 1‑in‑20 requests are slower — often enough to show up in dashboards and support tickets.
p99 / p99.9
Your worst active sessions and your biggest customers (they make many requests, so they always hit the tail). SLOs live here.
Fan‑out amplifies the tail
In a microservice request that fans out to many leaves, the request is only as fast as its slowest dependency. If one request touches 100 services and each has a p99 of 250 ms, the chance that none of them is in its slow tail is tiny — so the tail of a leaf becomes the common case for the whole request.
With 100 independent leaves each fast 99% of the time, P(no leaf is slow) = 0.99¹⁰⁰ ≈ 0.37. So ~63% of requests hit at least one p99 tail. You can’t just optimise the tail away — you tolerate it: hedged requests (send a second copy after a short delay, take the first answer), request‑level timeouts, and returning partial results when a non‑critical leaf is slow.
Many load generators pause sending while a request is stalled, so they never record how long waiting callers would have suffered — under‑reporting the tail badly. Fix it by measuring against the intended send time (back‑filling the gap), as tools like wrk2 and HdrHistogram do. Otherwise your “p99” is a comforting fiction.
Timeouts & deadline propagation
An unbounded wait is a resource leak waiting to become an outage: threads, connections and memory pile up behind one stuck dependency until the whole service falls over. Every remote call needs a timeout. Distinguish the kinds:
| Timeout | What it bounds | Typical value |
|---|---|---|
| Connect | Establishing the TCP/TLS connection | 50–200 ms |
| Read / socket | Gap between bytes once connected | 100 ms – 2 s |
| Total / deadline | End‑to‑end wall clock for the whole operation | the request’s budget |
The powerful idea is a deadline (budget) propagated across hops. The client sets one absolute deadline; every downstream call gets whatever time remains, minus a small safety margin. A hop that sees the budget already exhausted fails fast instead of doing doomed work.
// One absolute deadline, computed once at the edge
deadline = now() + total_budget // e.g. now + 1000ms
function call(downstream):
remaining = deadline - now()
if remaining <= safety_margin:
throw DeadlineExceeded // don't start work we can't finish
downstream.timeout = remaining - safety_margin
return downstream.send()
A single null/0/“no timeout” on a shared client is a classic root cause: one slow dependency exhausts the connection pool, callers queue, threads block, and the outage cascades upstream. Default every client to a finite timeout and a deadline, and make “no timeout” impossible to configure.
Retries, exponential backoff & jitter
Retries paper over transient faults (a dropped packet, a brief 503, a leader election). But retry the wrong thing and you amplify an outage. Two rules gate every retry:
✅ Retry when…
- The operation is idempotent (GET, PUT, DELETE, or a POST guarded by an idempotency key).
- The failure is transient: timeouts, connection resets, 429, 502/503/504.
- You have budget: total attempts and time are bounded.
🚫 Don’t retry when…
- The op is not idempotent and you can’t dedup (you’ll double‑charge).
- The error is deterministic: 400, 401, 403, 404, 422 — retrying just repeats the failure.
- You’d retry immediately, or forever — that’s a self‑inflicted DDoS.
The backoff formula
Grow the delay geometrically and cap it: delay = min(cap, base · 2^attempt). With base = 100ms and cap = 6.4s the delays are 100, 200, 400, 800, 1600, 3200, 6400 ms… This spaces out retries so a struggling backend gets breathing room instead of a wall of duplicates.
Why jitter is non‑negotiable
Without jitter, every client that failed at the same instant retries at the same instant — a synchronised retry storm (thundering herd) that hits the recovering service with a perfectly aligned spike and knocks it back down. Adding randomness spreads the retries out. Two standard schemes (from the AWS “backoff and jitter” playbook):
temp = min(cap, base * 2**attempt)
# Full jitter — pick anywhere in [0, temp]
sleep = random_between(0, temp)
# Equal jitter — keep half, randomise the other half
sleep = temp/2 + random_between(0, temp/2)
# Decorrelated jitter — walk based on the previous sleep
sleep = min(cap, random_between(base, prev_sleep * 3))
Even with backoff, uncapped retries turn a partial outage into a total one: retried traffic can be several× the original load exactly when the system is weakest. Impose a retry budget — cap retries to a small fraction of live traffic (e.g. “retries ≤ 10% of requests”), and shed the rest. Also avoid retry amplification: retries at every layer multiply, so retry at one layer and pass failures up.
Circuit breakers (reliability lens)
A circuit breaker stops you from hammering a dependency that is already down, converting slow cascading failures into fast, cheap rejections that protect the error budget. It’s a small state machine wrapped around a call. (The full mechanics live in Chapter 1; here’s the reliability‑relevant recap.)
Fail fast
When Open, return instantly (or a fallback) instead of tying up threads waiting on a dead dependency — that’s what stops the cascade.
Bulkheads
Isolate resources per dependency (separate pools/queues) so one failing dependency can’t drain the whole service’s capacity.
Load shedding
Under overload, reject low‑priority work early (at the edge) to keep the system’s core functions within SLO.
Idempotency (reliability lens)
Networks give you at‑least‑once delivery by default: a client can’t tell a lost request from a lost response, so it retries — and the server may process the same operation twice. The cure is making operations idempotent: repeating them has the same effect as doing them once.
The client attaches a unique key (e.g. Idempotency-Key: <uuid>) per logical operation. The server records the key with the first result; a retry with the same key returns the stored result instead of re‑executing. This turns a dangerous “charge the card again” into a safe no‑op. Full patterns are in Chapter 2 (API design) and Chapter 1.
Retries and idempotency are two halves of one idea: you can only safely retry an operation that is idempotent (or dedup‑protected). That’s why the retry rules above start with “idempotent + transient”. Without idempotency, every retry is a gamble on duplication.
Incident handling
When the budget is burning, process beats heroics. Classify severity, mitigate first, and learn without blame.
| Severity | Impact | Response |
|---|---|---|
| SEV1 | Major outage / data loss / core flow down for many users | Page immediately, all‑hands, incident commander, comms to customers |
| SEV2 | Significant degradation, a region or feature down, SLO at risk | Page on‑call, dedicated responder, status update |
| SEV3 | Minor / contained, workaround exists, budget mostly intact | Ticket, handle in business hours |
The lifecycle
- Detect. Alerting (burn‑rate SLO alerts, health checks, synthetics) surfaces the problem. The clock on MTTD (Mean Time To Detect) starts here.
- Triage & acknowledge. On‑call ack’s the page — that stops MTTA (Mean Time To Acknowledge) — sets a severity, and — for SEV1/2 — names an Incident Commander who coordinates while others investigate.
- Mitigate. Stop the bleeding before understanding it: roll back the deploy, fail over, shed load, disable the bad feature flag. Restoring the SLO is the goal, not the root cause.
- Resolve. Confirm the SLI is healthy and the budget has stopped burning. MTTR (Mean Time To Recovery/Repair) stops here.
- Learn. Write a blameless postmortem: timeline, contributing factors (systems, not people), and concrete action items with owners. Feed fixes back into runbooks and alerting.
The MTT‑ metrics
They all read as “Mean Time To…” — an average measured over many incidents: MTTD = to Detect · MTTA = to Acknowledge · MTTR = to Recover/Repair. The odd one out is MTBF = Mean Time Between Failures (how long it stays healthy). Track them to see whether you’re getting faster at each stage.
Runbooks
Every alert links to a runbook: what it means, how to confirm, first mitigations. Turns a 3 a.m. page into a checklist, not a research project.
Blameless culture
Assume competent people acting on the information they had. Blame hides the real (systemic) causes and teaches people to stop reporting.
Error‑budget policy
The governance loop: healthy budget → ship features; exhausted budget → freeze launches and invest in reliability until recovered.
“Do you root‑cause or mitigate first?” Mitigate. Restore service (roll back / fail over / shed) to stop burning the budget and end customer pain, then investigate the root cause calmly. Debugging a live SEV1 in prod while users suffer is how a 10‑minute blip becomes an hour.
Memorise this
Cheat sheet — Reliability
- The four terms: SLI (measured) → SLO (internal target) → SLA (external, looser, has penalties).
error budget = 1 − SLO. - The number: 99.9% monthly ≈
43 minbudget. 99.99% ≈4.3 min. Three nines = 8.76 h/yr; four nines = 52.6 min/yr. - Serial deps multiply:
0.999¹⁰ ≈ 99.0%— minimise the critical path. - Tail rules: report
p50/p95/p99, never the mean. Fan‑out means0.99¹⁰⁰ ≈ 0.37→ ~63% hit a p99. Hedge and tolerate tails. - Timeouts: finite everywhere; propagate one deadline, each hop passes the remainder; fail fast when the budget is gone.
- Retries: only idempotent + transient.
delay = min(cap, base·2^attempt)+ jitter to avoid storms. Cap with a retry budget. - Protect the budget: circuit breaker + bulkheads + load shedding stop cascades.
- Incidents: mitigate → resolve → learn (blameless). Watch MTTD / MTTA / MTTR.