Reliability Engineering
Chapter 3

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

SLISLOSLAError budget p95 / p99BackoffTimeoutsIncidents

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.

How they nest: SLI → SLO → SLA

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.

100% — ideal SLO 99.95% internal target SLA 99.9% external floor measured SLI (healthy, above SLO) error budget = 1 − SLO (spend it on launches) below the SLA → penalties & credits reliability →
The SLI is measured; the SLO is a stricter internal target; the SLA is the looser external floor. Everything between the SLO and 100% is the error budget.

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
Error‑budget policy (say this)

“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 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 classBudget consumedLong / short windowBurn rateAction
Fast‑burn2% of monthly budget1 h & 5 min14.4×Page on‑call now
Medium‑burn5% of monthly budget6 h & 30 minPage (business hours OK)
Slow‑burn10% of monthly budget3 d & 6 hOpen 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.

AvailabilityNicknameDowntime / yearDowntime / monthDowntime / day
90%one nine36.5 days72 hours2.4 hours
99%two nines3.65 days7.2 hours14.4 min
99.9%three nines8.76 hours43.2 min1.44 min
99.95% 4.38 hours21.6 min43.2 s
99.99%four nines52.6 min4.32 min8.64 s
99.999%five nines5.26 min25.9 s0.86 s
Availability multiplies across serial dependencies

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.

43.2m
99.9% monthly budget
4.3m
99.99% monthly budget
99.0%
0.999¹⁰ (10 serial hops)
×
serial deps multiply

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.

AZ vs Region — the trade‑off in one line

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.

In plain English — no math needed

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.

50 100 150 200 250 300 350 request latency (ms) frequency mean ≈ 55ms p50 20ms p95 80ms p99 250ms ← the tail lives out here
The same distribution: the median is 20 ms but the mean is ~55 ms and p99 is 250 ms — 12× the median. Optimising the mean would miss the users who actually suffer.

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.

Request 1 → 100 leaves leaf · fast (p50) leaf · fast (p50) leaf · fast (p50) leaf · SLOW (p99) leaf · fast (p50) Response = slowest leaf
The response waits for the slowest leaf, so a leaf’s tail dominates the parent’s latency.
“p99 of a leaf becomes p50 of 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.

Coordinated omission (the measurement trap)

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:

TimeoutWhat it boundsTypical value
ConnectEstablishing the TCP/TLS connection50–200 ms
Read / socketGap between bytes once connected100 ms – 2 s
Total / deadlineEnd‑to‑end wall clock for the whole operationthe 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()
deadline · t = 1000ms t = 0 Client → Gateway · remaining 1000ms → Service A · remaining 960ms → Service B · budget 850ms remaining budget passed downstream time already consumed at this hop 50ms safety margin
One absolute deadline, propagated: each hop consumes some time and hands the remainder (minus margin) to the next. Nobody waits past t=1000ms.
Never set an infinite timeout

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.

0 1600 3200 4800 6400 delay (ms) 012 345 67 attempt number cap = 6400ms no jitter · base·2ⁿ (capped) equal‑jitter range [temp/2, temp]
Delay doubles each attempt until it hits the cap. Jitter randomises the exact delay within the shaded band so retries desynchronise.

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))
Retry storms & the retry budget

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

CLOSED calls flow · counting failures OPEN reject fast · fail cheap HALF‑OPEN let one probe through failures ≥ threshold after cooldown probe ok probe fails
Closed → Open on repeated failures; Open → Half‑Open after a cooldown; a single probe decides whether to close again or trip back open.

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.

Dedup with an idempotency key

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.

The link to retries

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.

SeverityImpactResponse
SEV1Major outage / data loss / core flow down for many usersPage immediately, all‑hands, incident commander, comms to customers
SEV2Significant degradation, a region or feature down, SLO at riskPage on‑call, dedicated responder, status update
SEV3Minor / contained, workaround exists, budget mostly intactTicket, handle in business hours

The lifecycle

  1. Detect. Alerting (burn‑rate SLO alerts, health checks, synthetics) surfaces the problem. The clock on MTTD (Mean Time To Detect) starts here.
  2. 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.
  3. 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.
  4. Resolve. Confirm the SLI is healthy and the budget has stopped burning. MTTR (Mean Time To Recovery/Repair) stops here.
  5. 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.

Common interview trap

“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 min budget. 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 means 0.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.
Say this: “I define reliability as an SLO on a specific SLI — say 99.9% of requests under 250 ms over 28 days — and I manage to the error budget: while it’s healthy we ship, when it burns we freeze and fix.”
Say this: “I optimise the tail, not the mean. With fan‑out, a leaf’s p99 becomes the request’s common case, so I use timeouts, hedged requests and partial results to keep p99 in SLO.”
Say this: “Every remote call has a finite timeout and a propagated deadline, retries are idempotent‑only with exponential backoff plus jitter and a retry budget, and a circuit breaker fails fast so one sick dependency can’t take everything down.”