Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

24 chapters
⌕
Beginner7›
B1The mental modelB2Client-server basicsB3Vertical vs horizontalB4Databases in designB5Caching fundamentalsB6APIs & communicationB7Walkthrough: URL shortener
Intermediate9›
I1Load balancing in depthI2CDNsI3Message queues & asyncI4Consistency modelsI5Rate limiting & throttlingI6Designing for availabilityI7Storage systemsI8Search systemsI9Walkthrough: feed/chat
Advanced8›
A1CAP theorem in depthA2Sharding at scaleA3Distributed consensusA4Fault toleranceA5Observability at scaleA6Capacity estimationA7Case studiesA8Tradeoff thinking
/ search[ ] chaptert top

System Design levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
A5

Observability at scale

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

Three pillars, three questions, one fixed 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 reach for them in a fixed 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. 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.

The three pillars, priced

PillarQuestion it answersShapeCost driverTypical retentionHow 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 — roughly $0.30-$1.00 per GB on managed platforms 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 is dominated by the tail it is hiding — you can double your p99 and barely move the mean.

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
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's "tail at scale" framing 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 The standard fix 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 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 quietly creates a new series every deploy and never garbage-collects the old ones. Each one 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 standard is a single header:

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.

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   // for 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 the fourth is the leading indicator you use for capacity planning, not for paging. Keep cause metrics — you need them the moment you start debugging — but put them on dashboards, not on the pager.

Don't page on this (cause)Page on this instead (symptom)Why
CPU above 80% on app tierp99 request latency above the SLO thresholdHigh 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, automated failover, no manual step in the recovery path.
99.99%4.3 minutes52.6 minutesMulti-region active-active. 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.

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.

The short window is the part people forget: it exists so that when the incident ends, the alert clears within minutes instead of staying lit for the remaining hour of the long window. And the error budget 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.
←previousFault tolerance↑ CovernextCapacity estimation→