Observability & SLOs · How it works

9 min read
Senior14 min read
Rapid overview

How it works

Monitoring vs observability

Monitoring answers questions you predicted: dashboards and alerts on known failure modes. Observability is being able to ask questions you did not predict without shipping new code — which needs high-dimensional, well-correlated telemetry (request attributes on traces and structured logs, shared trace ids). In practice you need both: monitoring tells you something is wrong, observability tells you why.

Q: What is the difference between monitoring and observability?

A: Monitoring checks for conditions you anticipated — known metrics, known thresholds, known dashboards — and tells you that something is wrong. Observability is the property that lets you investigate conditions you did not anticipate, by slicing rich telemetry by arbitrary dimensions (customer, region, version, endpoint) and following a request across services. A system can be well monitored and still hard to debug if its telemetry cannot be broken down along the dimension the new problem lives in.

The three signals

SignalWhat it isCheap forExpensive for
MetricsNumeric time series with labelsDashboards, alerting, long retentionHigh-cardinality breakdowns
LogsTimestamped events, ideally structured JSONExact details of one eventVolume at scale, aggregation
TracesSpans of one request across services, linked by a trace idLatency breakdown, dependency mapsStoring every request (so sampled)

Correlate them: put the trace id in every log line, attach exemplars (trace ids) to latency metrics, and use the same attribute names across all three.

Q: Why should every log line carry the trace id?

A: So that you can jump between signals. A latency alert points at a slow trace; the trace shows which service and span were slow; the trace id then retrieves every log line written for that request across all services, including the error messages and parameters that metrics and spans do not carry. Without the shared id, correlating logs across services means guessing by timestamp, which fails under concurrency.

RED and USE

  • RED, for every request-driven service: Rate (requests per second), Errors (failed requests per second or as a ratio), Duration (latency distribution — percentiles, not the average).
  • USE, for every resource (CPU, memory, disk, network, connection pool, queue): Utilisation (percent busy), Saturation (work waiting — run queue, queue depth, pool waiters), Errors.
  • Google's four golden signals combine them: latency, traffic, errors, saturation.
Q: Why should latency be reported as percentiles rather than an average?

A: Because latency distributions are skewed and the average hides the tail. A service where 95% of requests take 20 ms and 5% take 2 seconds has an average around 120 ms, which describes no real request. Users feel the tail: a page that makes 20 backend calls hits the p95 of each call often. Report p50, p95 and p99 (and track the maximum or p99.9 for critical paths), computed from histograms so they can be aggregated across instances — averaging per-instance percentiles is mathematically wrong.

Cardinality

Each unique combination of metric name and label values is a separate time series. http_requests_total{method, route, status} with 5 × 50 × 10 values is 2,500 series — fine. Add user_id with a million values and it becomes 2.5 billion, which will crash or bankrupt the metrics backend. Rules:

  • Labels must have bounded, small value sets: route templates (/users/:id), not raw paths; status class, not full messages.
  • Put high-cardinality attributes (user id, order id, request id) on traces and logs, which are built to store them per event.
  • Watch series count as a metric of its own.
Q: Why is putting a user id in a metric label dangerous?

A: Because every distinct label value creates a separate time series that the metrics system must index, store and query. A user id with millions of values multiplies the series count by millions, exhausting the memory of Prometheus-style systems or generating a large bill in hosted ones, and making queries slow. Per-user detail belongs in traces or structured logs, which store attributes per event; metrics should use bounded labels such as route, status class, region or plan.

Distributed tracing

sequenceDiagram participant G as Gateway participant O as Orders participant P as Payments participant DB as Database G->>O: POST /orders with traceparent trace 4bf9, span a1 O->>P: charge with traceparent trace 4bf9, span b2 P->>DB: insert payment, span c3 DB-->>P: ok 12 ms P-->>O: ok 180 ms O-->>G: 201 in 240 ms Note over G,DB: All spans share trace 4bf9 - the backend rebuilds the tree

Each service creates spans for its work and propagates the context (W3C traceparent header, or message headers for queues) to the next hop. The tracing backend assembles spans by trace id into a tree showing where time was spent. OpenTelemetry is the vendor-neutral standard for the SDKs and wire format.

Tracing everything is expensive, so traces are sampled: head sampling decides at the start (keep 1%); tail sampling decides after the trace completes, so it can keep all errors and slow requests while dropping most fast successful ones.

Q: What is the difference between head-based and tail-based trace sampling?

A: Head-based sampling decides at the start of a request whether to record its trace, typically a fixed percentage, and propagates the decision downstream. It is cheap and simple but blind: a rare error is as likely to be dropped as a boring success. Tail-based sampling buffers all spans of a trace until it completes, then decides — keeping every error, every slow request, and a small share of normal ones. It captures the interesting traces but needs a collector tier with memory to hold in-flight traces.

Q: How does trace context cross an asynchronous boundary such as a message queue?

A: The producer injects the current trace context into the message headers or attributes, exactly as it would into HTTP headers, and the consumer extracts it when it processes the message and starts its span as a child of, or a link to, the producer's span. Because the consumer may run seconds or hours later, and one batch may contain messages from many traces, span links are often used rather than a strict parent-child relationship. Without this, every consumer starts a new unrelated trace and the request's path stops at the queue.

SLIs, SLOs, SLAs and error budgets

  • SLI (indicator): a ratio of good events to valid events, measured where users experience it. "Proportion of checkout requests that return non-5xx in under 500 ms."
  • SLO (objective): a target for the SLI over a window. "99.9% over 28 days."
  • SLA (agreement): a contract with consequences (credits) — set looser than the SLO so you breach the SLO first.
  • Error budget: 1 − SLO. At 99.9% over 28 days with 10 M requests, 10,000 bad requests are allowed. When the budget is healthy, ship faster; when it is spent, prioritise reliability.
flowchart TD SLI[SLI - good requests over valid requests] --> SLO[SLO - 99.9 percent over 28 days] SLO --> EB[Error budget - 0.1 percent of requests may fail] EB --> H{Budget remaining} H -->|healthy| F[Ship features, run experiments] H -->|burning fast| A[Page on-call] H -->|exhausted| R[Freeze risky releases, fix reliability]
Q: What is the difference between an SLI, an SLO and an SLA?

A: An SLI is the measurement — the fraction of valid requests that were good, for a definition of good such as "succeeded within 300 ms". An SLO is the internal target for that measurement over a window, such as 99.9% over 28 days. An SLA is an external contract, usually with financial penalties, promising a level of service to customers; it is set looser than the SLO so the team is alerted and acts before a contractual breach.

Q: How does an error budget change how a team makes decisions?

A: It turns reliability from an argument into a number both product and engineering agreed on. If the SLO is 99.9%, then 0.1% of requests may fail, and that allowance can be spent on releases, experiments and migrations. While budget remains, the team ships at full speed; when a period's budget is exhausted, the agreed policy kicks in — freeze risky launches, prioritise reliability work — until it recovers. It also argues against over-investing: reliability far above the SLO is budget left unspent that could have bought velocity.

Alerting on burn rate

Alert on symptoms users feel — the SLI — not on every cause (CPU at 90% may be harmless). Burn rate is how fast the budget is being consumed relative to the pace that would exactly exhaust it by the end of the window: a burn rate of 1 uses the whole 30-day budget in 30 days; 14.4 uses 2% of it in one hour.

Multi-window, multi-burn-rate alerts (from the Google SRE workbook):

  • Page: burn rate ≥ 14.4 over 1 hour and over 5 minutes (fast, severe).
  • Page: burn rate ≥ 6 over 6 hours and over 30 minutes.
  • Ticket: burn rate ≥ 1 over 3 days (slow leak).

The short window makes the alert stop soon after the problem is fixed.

flowchart LR E[Error ratio over window] --> B[Burn rate - error ratio divided by budget ratio] B --> P1{14.4 or more over 1h and 5m} B --> P2{6 or more over 6h and 30m} B --> T1{1 or more over 3 days} P1 -->|yes| PG[Page] P2 -->|yes| PG T1 -->|yes| TK[Ticket]
Q: What is burn rate and why is it better to alert on than a raw error rate threshold?

A: Burn rate is the ratio between the current error rate and the error rate the SLO allows. At a 99.9% SLO the allowed rate is 0.1%, so a 1.44% error rate is a burn rate of 14.4 — if it continued, the 30-day budget would be gone in about two days. Alerting on burn rate ties pages directly to the promise made to users: a brief spike that uses little budget does not page, a sustained moderate problem does, and the thresholds mean the same thing on every service regardless of its traffic.

Q: Why should you page on symptoms rather than causes?

A: Because causes are many and often harmless, while symptoms are what users feel. CPU at 95%, one replica down or a queue growing may self-heal or be absorbed by redundancy; paging on each trains on-call to ignore pages. An SLO-based symptom alert — checkout success below target, latency budget burning — fires only when users are affected and covers causes nobody anticipated. Cause-based signals still belong on dashboards and in low-urgency tickets to aid diagnosis.

What to instrument when designing a system

In a design interview, close with: RED metrics per service and per critical endpoint; USE metrics for databases, caches, queues (consumer lag) and pools; traces across the request path with context propagated through queues; structured logs with trace ids; two or three SLOs on the user journeys that matter, with burn-rate alerts; and a dashboard per journey, not per server.

Q: In a system design interview, how would you describe observability for a design in one minute?

A: Name the user journeys and their SLOs first — for example 99.9% of checkouts succeed within 1 second over 28 days — and say alerts page on error-budget burn rate. Then list RED metrics for every service and endpoint, USE and saturation metrics for the stateful parts (database connections, cache hit ratio, queue consumer lag), distributed tracing with OpenTelemetry propagated across HTTP and messages with tail sampling to keep errors and slow requests, and structured logs carrying the trace id. Finish with the one or two metrics specific to this design's riskiest component.

See also