Skip to the chapter
←System DesignGGroundworkSDSystem Design
System Design/Advanced/A9
29 / 36 written chapters
/

The cover
  1. B1The mental model
  2. B2Client-server basics
  3. B3Vertical vs horizontal
  4. B4Databases in design
  5. B5Caching fundamentals
  6. B6APIs & communication
  7. B7Walkthrough: URL shortener
  1. I1Load balancing in depth
  2. I2CDNs
  3. I3Message queues & async
  4. I4Consistency models
  5. I5Replication
  6. I6Rate limiting & throttling
  7. I7Designing for availability
  8. I8Storage systems
  9. I9Search systems
  10. I10Microservices
  11. I11Security basics
  12. I12Walkthrough: feed/chat
  13. I13Walkthrough: notifications
  1. A1CAP theorem in depth
  2. A2Transactions and isolation
  3. A3Storage engines
  4. A4Sharding at scale
  5. A5Time, ordering and IDs
  6. A6Distributed consensus
  7. A7Batch & stream processing
  8. A8Fault tolerance
  9. A9Observability at scale
  10. A10Release safety
  11. A11Capacity estimation
  12. A12Case studies
  13. A13Walkthrough: key-value store
  14. A14Walkthrough: web crawler
  15. A15Walkthrough: payments
  16. A16Tradeoff thinking

SDA9 · Part 3 · Advanced

Observability at scale

Metrics say something is wrong, traces say where, logs say what — and p99 says whether anyone noticed.

19 min read3 diagrams10 sections0 exercises
29 of 36

In this chapter

  • Three pillars, three questions, one common order
  • The three pillars, priced
  • Averages lie, and they lie in the direction that hurts
  • Fan-out: why p99 per service is not p99 per user
  • Cardinality is the thing that makes metrics expensive

and 5 more

about 19 min
10On this page
  1. Three pillars, three questions, one common order
  2. The three pillars, priced
  3. Averages lie, and they lie in the direction that hurts
  4. Fan-out: why p99 per service is not p99 per user
  5. Cardinality is the thing that makes metrics expensive
  6. Distributed tracing and context propagation
  7. Structured logging, and the volume problem behind it
  8. Alert on symptoms, not causes
  9. SLOs and error budgets: making the alert threshold non-arbitrary
  10. Recognizing it in an unseen problem

Three pillars, three questions, one common order

Observability is usually taught as a list of three data types, which is useless at a whiteboard. The useful framing is that each answers a different question, and during an incident you usually reach for them in a familiar order. Metrics answer "is something wrong, and since when?" Traces answer "where in the call graph is it wrong?" Logs answer "what exactly happened to the request that failed?" Run that order backwards and you spend the outage grepping. It is a default drill-down, not a law: teams that store one wide, structured event per request (what some call "observability 2.0") can slice by any field and jump straight to the evidence, and "three pillars" is itself a contested label. Interviewers ask about this because a design that cannot be debugged is not a finished design — and because the candidate who says "and here is how I'd know it broke" is visibly a different animal from one who draws boxes and stops.

api gateway feed service ranking svc collector (sidecar agent) metrics — TSDB 13 months, cheap* logs — object store 14 days, expensive* traces 7 days, sampled* alerting pager
Notice the one red arrow: only metrics drive the pager. Logs and traces are for the human who has already been woken up — paging off a log line is how you build an alert nobody trusts. (*Retention periods are illustrative, not standards.)

The three pillars, priced

PillarQuestion it answersShapeCost driverTypical retention (illustrative)How it fails you
Metrics Is something wrong, and when did it start? Numeric time series, fixed label set, pre-aggregated at write Unique time series (cardinality), not event count 13-15 months (downsampled) Tells you the error rate is 4%. Cannot tell you which 4%.
Logs What exactly happened to this one request? Semi-structured records, unbounded fields Bytes ingested and indexed — priced per GB and vendor-dependent (for example, Amazon CloudWatch Logs lists standard ingestion at $0.50 per GB in US East) 7-30 days hot, then cold storage Volume. At real scale you cannot afford to keep them all, so the one you needed was sampled away.
Traces Where in the call graph did the latency go? A tree of timed spans sharing a trace id Spans retained — so almost always sampled at 0.1%-10% 3-14 days Head-based sampling throws away the slow trace before it knows it was slow.

A fourth thing is worth naming because senior candidates do: events — a durable, structured record of a business fact (order placed, payment captured). Those are not telemetry, they belong in the same queue infrastructure as everything else in the message-queue chapter, and they are the thing you reconcile against when metrics and reality disagree.

Averages lie, and they lie in the direction that hurts

A mean latency is a single number summarising a distribution that is never symmetric. Request latency is always right-skewed: it has a hard floor (you cannot be faster than the network) and no ceiling (a lock, a GC pause, a cold cache, a retry). Averaging a floor with a fat tail produces a number that describes no actual user. Worse, the mean blends the fast majority and the slow tail into one number that hides both: if the slowest 1% of requests all got twice as slow (p99 going from 1.9 s to 3.8 s), the mean would rise by only about 19 ms, from 205 ms to roughly 224 ms, while the p99 doubled.

p50 · 120 ms mean · 205 ms p99 · 1.9 s latency → the mean sits between p50 and p75 and describes nobody; the users who churn are all living out past that red line
Horizontal axis compressed, not to scale: at true scale the p99 marker would sit far off the right edge. The mean is pulled right by the tail without ever reaching it. Report p50 for "typical", p99 for "worst thing a real person routinely experiences", and never report a mean latency as if it were a user experience.
Say it like this → "I'd set the SLO on p99, not average. Average latency is a number that improves when your slowest users give up and leave — p99 is the number that tracks whether the product feels broken."

Fan-out: why p99 per service is not p99 per user

Here is the arithmetic that separates people who have run a distributed system from people who have read about one. A user request that touches 10 backend services is only fast if all ten were fast. If each service independently has a 1% chance of being in its slow tail, the chance the request avoids every one of them is 0.99 to the tenth power.

Services on the critical pathP(all fast) = 0.99nP(at least one slow path)
10.9901.0%
50.9514.9%
100.9049.6%
1000.36663.4%

Read the last row again: with a hundred-way fan-out, a per-service p99 means most requests hit at least one slow path. This is why Jeff Dean and Luiz Barroso's "The Tail at Scale" (CACM, 2013) matters — as you decompose into more services, the user-visible p99 degrades even though every individual service's p99 is unchanged. Turn it around to get the design rule: to hold a user-facing p99, each of ten backends needs roughly p99.9, because 0.99910 is 0.990. Every layer you add moves the percentile you must engineer for one notch further out.

⚠ Retries and hedging cut the tail but multiply the load A standard fix, described in that paper, is a hedged request: if a replica hasn't answered by p95, fire a second request to another replica and take the first response. That converts a tail event into a p95-plus-a-bit event for about 5% extra traffic (the paper's figure for deferring to p95). The failure mode is that when the system is already degraded, everything exceeds p95, so every request hedges, load doubles, and you have built a retry storm. Always pair hedging with a circuit breaker and a budget ("at most 5% of requests may hedge") — the same discipline as the retry-budget discussion in the fault tolerance chapter.

Cardinality is the thing that makes metrics expensive

A metrics system does not charge you per event. Incrementing a counter a billion times costs essentially nothing. What it charges for is unique time series — one series per distinct combination of metric name and label values, each of which needs its own in-memory index entry and its own compressed chunk on disk. Cardinality is multiplicative:

http_requests_total{service, endpoint, status, region}

  services   50
  endpoints  200
  statuses   15
  regions    5
  ----------------------------------------------
  50 * 200 * 15 * 5 = 750,000 active series   // fine — a mid-size Prometheus handles this

at a 15s scrape interval:
  750,000 * 4 samples/min * 1,440 min = 4.3 * 10^9 samples/day
  at ~2 bytes/sample compressed = ~8.6 GB/day   // affordable

now add one label: user_id, 20,000,000 values
  750,000 * 20,000,000 = 1.5 * 10^13 series    // the cluster is dead

The rule is mechanical: a metric label must be bounded and low cardinality — something you could write out on a whiteboard. User ids, request ids, session ids, raw URLs with path parameters, email addresses, and error strings all belong in logs or traces, where you pay per event but the schema is free. "Which user?" is a logs question. "How many users?" is a metrics question.

⚠ The accidental-cardinality bugs are almost always the same three Un-templated URL paths as a label (/orders/8814 instead of /orders/:id); the raw exception message as a label when it contains an id or a timestamp; and a pod name or container id as a label in an autoscaling deployment, which creates a fresh series for every new pod. Prometheus does age out series that stop receiving samples, but the number of distinct series it has to hold, and the memory and index space that go with them, grows with churn rather than with the size of the current fleet. Each one of these has taken down a real monitoring cluster.

Distributed tracing and context propagation

A trace is one tree per user request. Each unit of work is a span carrying a trace id (shared across the whole request), its own span id, and a parent span id. The only genuinely hard part is context propagation: every hop — HTTP call, gRPC call, queue publish, thread handoff — must carry the ids forward, or the tree silently breaks into disconnected fragments. The W3C Trace Context standard (a W3C Recommendation, updated November 2021) defines a traceparent header, plus an optional tracestate header for vendor-specific data:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             ^   ^                                ^                ^
             |   trace-id (16 bytes)              parent span-id   flags
             version                              (8 bytes)        (01 = sampled)
one trace_id, propagated on every hop via the traceparent header api-gateway · 480 ms auth-svc · 40 ms feed-svc · 420 ms follow-db · 40 ms post-store · 60 ms ranking-svc · 295 ms 0 100 200 300 400 500 ms the dashboard said "feed-svc is slow" — the trace says feed-svc spent 70% of its time waiting on ranking
The value of the waterfall is not the total, it is the gaps and the nesting. Bars that start late reveal queueing; bars that overlap reveal parallelism you thought you had; a long parent with short children means the time went somewhere you are not instrumenting.
Sampling strategyHow it decidesCostReach for this when…
Head-based, fixed rateCoin flip at the entry point; the decision rides in the traceparent flags so the whole tree agreesCheapest; trivially statelessDefault. You want a representative sample of normal traffic and you accept losing most rare events.
Head-based, rate-limited per routeN traces per second per endpointCheap; needs a per-route counterTraffic is wildly skewed and a fixed rate would give you a million traces of the health check and none of checkout.
Tail-basedBuffer all spans of a trace at the collector, decide once it completes: keep if slow, errored, or rareCollector must hold every in-flight trace in memory for the duration; needs all spans of a trace routed to the same collectorYou specifically need the pathological traces — which, since those are the ones you debug, is most serious deployments.
Four pieces of vocabulary that make the rest practical OpenTelemetry is the vendor-neutral standard for generating and shipping all three signals: an instrumentation API and SDKs, the OTLP wire format, and a Collector that receives, processes (including tail-based sampling) and exports to whichever backend you choose. RED (rate, errors, duration; Tom Wilkie) is the per-service dashboard; USE (utilisation, saturation, errors; Brendan Gregg) is the per-resource checklist for hosts, disks and pools. Percentiles cannot be averaged: the mean of ten instances' p99 values is not the fleet's p99. Record latency as a histogram (counts per bucket), sum the buckets across instances, and compute the percentile from the sum; Prometheus summaries, which pre-compute quantiles per instance, cannot be aggregated. Exemplars attach a trace id to a histogram bucket, so you can click from "the p99 spike" straight to a slow trace.

Structured logging, and the volume problem behind it

A log line that is a sentence is a log line you can only grep. A log line that is an object is a log line you can query, aggregate and join to a trace. The single most valuable field is the trace id — it is what turns three independent stores into one investigation.

{"ts":"2026-08-08T11:04:22.184Z","level":"error","svc":"ranking",
 "trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7",
 "user_id":"u_88214","route":"/v1/feed","status":503,"dur_ms":1841,
 "err":"model_pool_exhausted","pool_size":32,"queue_depth":417}

Now the volume. At 100,000 requests/sec with one 500-byte log line each:

100,000 * 500 B          = 50 MB/s
50 MB/s * 86,400 s       = 4.3 TB/day
4.3 TB/day at $0.50/GB   ~ $2,150/day  ~ $780,000/year   // $0.50/GB is illustrative; one log line per request

That number is why sampling is not optional at scale, and why the sampling policy should be asymmetric: keep 100% of errors and warnings, keep 100% of anything on a trace that was already sampled, and keep 1% of successful requests. You lose almost nothing diagnostically and drop roughly 99% of the bill. Add a per-service log budget so one team's debug statement cannot consume the org's ingest quota — a noisy-neighbour problem identical in shape to the one solved in the rate-limiting chapter.

Alert on symptoms, not causes

The instinct is to alert on everything you can measure: CPU above 80%, disk above 70%, replica lag above 5 seconds. Every one of those is a cause alert, and cause alerts have two failure modes. They fire when nothing is wrong (CPU at 90% during a nightly batch job is correct behaviour), and they fail to fire when something is wrong in a way you did not predict. After six months of false pages, the team mutes the channel, and the monitoring system is now decorative.

Alert on symptoms instead: the things a user would complain about. Google's four golden signals are latency, traffic, errors, and saturation; the first three are symptoms, and saturation is the early warning. The SRE book's rule is to page when one signal is problematic or, in the case of saturation, nearly problematic: a disk or quota about to fill deserves a page before it causes errors. Page on that kind of imminent exhaustion, and leave a generic "CPU above 80%" on the dashboard. Keep cause metrics — you need them the moment you start debugging — but put them on dashboards, not on the pager. Every page should link to a runbook and have an owner, and every serious incident should end in a blameless postmortem; an alert that nobody can act on is noise, however well it was measured.

Don't page on this (cause)Page on this instead (symptom)Why
CPU above 80% on app tierLatency SLO burning its budget fast (multi-window burn rate on p99 requests)High CPU with acceptable latency is a well-utilised fleet, not an incident.
Read replica lag above 5 sRate of stale-read complaints / stale-read guard tripsLag only matters if a user reads their own write and doesn't see it.
Queue depth above 10,000Oldest-message age above 5 minutesDepth depends on message size and consumer count; age is directly the user-visible delay.
A single host is unreachableError rate above the error budget burn thresholdIf losing one host pages you, you did not build the fault tolerance you claimed.

SLOs and error budgets: making the alert threshold non-arbitrary

An SLI is the measurement ("proportion of requests served in under 300 ms"). An SLO is the target on it ("99.9% over a rolling 30 days"). The error budget is the leftover: 0.1% of requests, which over 30 days is a concrete allowance you may spend on deploys, experiments and bad luck. This is the mechanism that converts a philosophical argument about reliability into arithmetic.

Availability targetBudget per 30 daysBudget per yearWhat it implies
99%7.2 hours3.65 daysAn internal tool. One long maintenance window is fine.
99.9% ("three nines")43.2 minutes8.8 hoursA normal SaaS product. Achievable with a single region done well.
99.95%21.6 minutes4.4 hoursMulti-AZ done well, automated failover, no manual step in the recovery path.
99.99%4.3 minutes52.6 minutesMulti-AZ with no shared single point of failure; multi-region active-active can go further but costs far more, and bad deploys hit every region. A human cannot even read the page in the budget.
99.999%26 seconds5.3 minutesAlmost never the right answer. Say so out loud when someone asks for it.

Alerting on the raw SLI ("error rate above 0.1%") is too twitchy: a 30-second blip trips it. Alerting on the whole window is too slow: you find out you blew the budget on day 29. The standard answer is multi-window burn-rate alerting — page on how fast the budget is being consumed, measured over both a long and a short window so a recovered blip stops paging. The table below is the recommended policy for a 99.9% SLO from the Google SRE Workbook chapter "Alerting on SLOs"; the short window is about one twelfth of the long one.

Burn rateBudget consumedWindows (long + short)Response
14.4×2% in 1 hour1 hour + 5 minutesPage immediately. At this rate the month is gone in ~2 days.
6×5% in 6 hours6 hours + 30 minutesPage.
1×10% in 3 days3 days + 6 hoursFile a ticket. This is a slow leak, not an outage.

Dry run: a 20-minute incident through a multi-window burn-rate alert

SLO: 99.9% over 30 days → an allowed error rate of 0.1%. Page when both the 1-hour and the 5-minute burn rate exceed 14.4× (an error rate of 1.44%). Traffic is steady; baseline error rate is 0.05%; the incident runs 10:00–10:20 at a 5% error rate.

TimeWhat's happening5-min error rate → burn rate1-hr error rate → burn rateBoth exceed 14.4×?Alert state
t = 09:55Baseline, healthy0.05% → 0.5×0.05% → 0.5×NoQuiet
t = 10:055 minutes into the incident5% → 50×0.46% → 4.6× (5 bad minutes of the last 60)No — long window hasn't caught upStill quiet
t = 10:1717 minutes in5% → 50×1.45% → 14.5× (17 bad minutes of 60)Yes — the 1-hour window just crossed 14.4× (it needs about 16.9 bad minutes)Pages
t = 10:20Incident ends5% → 50×1.70% → 17× (20 bad minutes of 60)YesStill paging
t = 10:233 healthy minutes since the end2.03% → 20× (2 bad minutes still in the 5-minute window)1.70% → 17×YesStill paging
t = 10:244 healthy minutes since the end1.04% → 10.4× (1 bad minute left)1.70% → 17× (the bad 20 min are still inside the trailing hour)No — short window recoveredClears (the crossing is at about 3.6 minutes after the end)

The alert only fires once both windows agree, which is why it waits until about 10:17 rather than paging on the first bad minute — and it clears at about 10:24 even though the 1-hour window is still showing a 17× burn, because the 5-minute window is what tells the alert the incident is actually over. Without that short window, this page would stay lit for most of the remaining hour after the problem was already fixed. The price of the long window is the 17-minute wait; a total outage burns the budget far faster than 14.4× and trips the same rule in about 1 minute (a 100% error rate gives a burn rate of 1000×, and the 1-hour window crosses 14.4× after about 0.83 bad minutes).

The error budget also has a second, political job — when it is exhausted, feature launches stop until reliability work restores it. That is the only mechanism anyone has found that makes "we should invest in reliability" an automatic decision rather than a quarterly argument.

Say it like this → "I'd define the SLI as the fraction of feed requests served under 300 ms at the edge, set the SLO at 99.9% over 30 days, and page on a 14.4× burn rate over a one-hour window with a five-minute short window to suppress recovered blips. Everything else — CPU, replica lag, queue depth — goes on a dashboard, not the pager."

Recognizing it in an unseen problem

  • The prompt says "highly available", "five nines", "SLA", or asks how you would operate the system — that is an invitation to talk SLI/SLO/error budget, and most candidates skip it entirely.
  • A naive design monitors hosts (CPU, memory, disk) and calls it observability. The tell of a senior answer is monitoring user-visible symptoms and treating host metrics as debugging aids.
  • Any design with more than about five services on the critical path has a tail-latency problem by construction — bring up the 0.99n arithmetic unprompted; it is one of the highest-signal thirty seconds available to you.
  • If you propose adding a label to a metric, immediately state its cardinality bound. If it is unbounded, it is a log field, not a label.
  • Distinguish it from the fault-tolerance chapter: fault tolerance is about the system surviving a failure; observability is about humans finding out and locating it. A system can be perfectly redundant and completely un-debuggable.
  • The pitfall: proposing logging everything. At 100k rps that is a multi-hundred-thousand-dollar annual line item. Name the sampling policy (all errors, 1% of successes) before the interviewer has to ask what it costs.
Before you move on
  • Say, in order, which of metrics, traces or logs you'd reach for first during an incident, and why running that order backwards means grepping.
  • Walk through the burn-rate dry run above and explain why the page fires at about 10:17 rather than 10:05, and clears at about 10:24 although the 1-hour window still shows 17×.
  • Compute the chance that a request fanning out to 10 services is fast on all of them when each has a 1% chance of landing in its slow tail (about 90%), and say which per-service percentile you need to hold a 99% user-facing target (p99.9).
  • Explain why a metric label needs a cardinality bound but a log field doesn't, using the user_id example.
  • Name two things you would alert on and two you would only dashboard, and justify the split using the symptom-vs-cause distinction.
Sources and further reading
  • Google SRE book: monitoring distributed systems — the four golden signals, symptoms versus causes, and paging on nearly-saturated resources
  • Google SRE Workbook: alerting on SLOs — the multi-window burn-rate table (14.4x, 6x, 1x) used in this chapter
  • The Tail at Scale (Dean and Barroso, 2013) — why fan-out amplifies tail latency and how hedged requests help
  • W3C Trace Context — the traceparent and tracestate headers and the sampled flag
  • OpenTelemetry overview — vendor-neutral signals, the OTLP format and the Collector
  • Prometheus: histograms and summaries — why quantiles cannot be averaged but histogram buckets can be summed
  • Prometheus feature flags (exemplar storage) — exemplars as references from metrics to trace ids
  • Amazon CloudWatch pricing — one real per-GB log ingestion price to anchor the cost arithmetic
  • The USE method (Brendan Gregg) — utilisation, saturation and errors for every resource
  • The RED method (Grafana Labs) — rate, errors and duration for every service

Claims in this chapter were checked against these sources in October 2026. Figures marked "planning figure" are rules of thumb, not guarantees.

✓Practise

5 questions drawn from 8, scored out of 5. This is practice: it runs in your browser, saves nothing and does not change what you have marked read.

A9

Finished reading?

0 of 36 chapters read in this series

→Next stop

  1. A8Previous · A8Designing for fault tolerance
  2. A9you are here
  3. Next · A10Release safety and incident responseMost outages follow a change. How to ship in small steps, watch the right signals, back out fast, and learn without blame.14 min read

01On this page

10 sections · 0%

  1. 01Three pillars, three questions, one common order
  2. 02The three pillars, priced
  3. 03Averages lie, and they lie in the direction that hurts
  4. 04Fan-out: why p99 per service is not p99 per user
  5. 05Cardinality is the thing that makes metrics expensive
  6. 06Distributed tracing and context propagation
  7. 07Structured logging, and the volume problem behind it
  8. 08Alert on symptoms, not causes
  9. 09SLOs and error budgets: making the alert threshold non-arbitrary
  10. 10Recognizing it in an unseen problem

[] chapters · t top