Engineering Atlas › Problem Solving & Incidents (AI era)

Debugging Distributed Systems

11 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

Where before what

A user sees a slow checkout. The checkout service's latency is up, but checkout calls inventory, pricing, payments and a fraud check, each of which calls others. Debugging checkout's code first is often wasted time. Localise first, as Google's troubleshooting chapter advises: stabilise the service, then ask what changed recently and bisect the system by checking the communication paths on each side of a split (SRE book, ch. 12). Which hop added the latency or produced the error? Distributed tracing answers this directly; without it, compare each dependency's latency and error metrics from the caller's side and the callee's side.

Traces

A trace is the tree of spans for one request. Each span records a service, an operation, start and end times, status and attributes, plus its parent span. The W3C Trace Context standard defines the traceparent header, four dash-separated fields (version, a 16-byte trace ID, an 8-byte parent span ID and flags), that carries them across HTTP calls (W3C Recommendation, 2021); message queues carry the same in message headers. Google's Dapper paper (2010) built its tracing around sampling and a small set of instrumented common libraries, which kept overhead low enough to run everywhere (Dapper). Two analyses matter most: self time (time a span spent in its own code, excluding its children) and the critical path (the chain of spans that determined the total duration; speeding up anything off it does not help).

Partial and gray failures

Distributed systems rarely fail cleanly. A Microsoft Research paper (HotOS 2017) named this "gray failure" and traced it to differential observability: failure detectors may not notice problems that applications plainly suffer (Gray Failure). Examples: a host whose disk is slow, a service that answers health checks but times out on real work. Signs: errors or latency concentrated on one host, zone, version or client. Always split your metrics: by host, availability zone, deploy version, dependency and customer. A 2 percent error rate overall might be 100 percent on one of fifty hosts.

Timeouts, retries and metastability

Timeouts and retries protect against transient failures but change system behaviour under stress. If every layer retries three times, a failure at the bottom of a five-layer stack can multiply load by 3^5 = 243 times. A brief slowdown causes timeouts, timeouts cause retries, retries add load, load keeps latency high, and the system stays down after the original trigger is gone. A HotOS 2021 paper called these metastable failures: a trigger pushes the system into a bad state that persists after the trigger is removed, because a sustaining effect, often work amplification, keeps goodput low. Retries are one of the most common sustaining mechanisms, and failures that end when the trigger ends, such as a plain DoS attack, are not metastable (Bronson et al.). Their example: a database that handles 300 queries a second, an app at 280 QPS with one retry per request, and a 10-second network blip. The retries double the offered load to 560 QPS, so the database never recovers until load drops or retries stop. The fixes are retry budgets (retry only if fewer than, say, 10 percent of recent requests were retries), exponential backoff with jitter, retrying at one layer only, and load shedding.

Clocks and ordering

Each machine's clock drifts; NTP typically keeps servers within a few milliseconds, sometimes worse. Two log lines from different hosts 2 ms apart may be in the wrong order. Use the trace's parent-child structure for causality, use per-key sequence numbers where order matters, and treat cross-host timestamp comparisons below the clock error as unknown.

The same shape everywhere

Localising a fault in a chain of dependencies is the same problem as finding the slow step in a business process or the bottleneck in a supply chain: measure each stage's own contribution, find the critical path, split by segment to find concentrated problems.

Down the stack

Layer 1The contract

A debuggable distributed system promises that every request carries a trace ID end to end, that every service emits metrics split by dependency, host and version, that logs include the trace ID, and that retries and timeouts follow a documented budget. With those in place, any user-visible symptom can be localised to a hop within minutes.

Layer 2The mechanism

Instrumentation libraries (OpenTelemetry SDKs) create spans automatically for incoming and outgoing HTTP, gRPC and database calls, inject traceparent into outgoing requests and extract it from incoming ones. Spans are exported to a collector that samples (head sampling decides at the start of a request; tail sampling decides after it completes, so it can keep all slow or failed traces) and stores them. Self time is computed by subtracting the union of child span intervals from a span's duration. The critical path is found by walking from the root, at each step following the child that finished last, since the parent could not finish before it.

Layer 3The machine

At the network level, a "slow dependency" can be many things: TCP retransmissions after packet loss (the first retransmission timeout on Linux is at least 200 ms, so 1 percent loss creates a long latency tail), connection pool exhaustion where requests wait for a socket, DNS lookups timing out, or TLS handshakes on new connections adding round trips. Tools such as ss -ti show per-connection retransmit counts and round-trip times; tcpdump shows what is really on the wire. A trace span that shows 200 ms on the client side but 5 ms on the server side means the time was spent between them: in queues, the network or connection setup.

Why it's built this way

  1. Requests cross many processes, so the cause is often far from the symptom.
  2. Each process sees only its own part, so correlation needs a shared ID propagated everywhere.
  3. Failures are partial and gray, so aggregates must be split to reveal them.
  4. Retries and timeouts interact across layers, so they need budgets and coordination.
  5. Clocks differ, so causality must come from structure, not timestamps.

Diagram

flowchart TD
  C["checkout 420 ms"] --> I["inventory 30 ms"]
  C --> P["pricing 45 ms"]
  C --> PAY["payments 330 ms"]
  PAY --> F["fraud-check 300 ms"]
  F --> DB["fraud DB 290 ms"]
  DB -.->|"critical path"| R["root cause candidate"]

Worked example

Checkout p99 latency rises from 400 ms to 2.5 seconds at 14:10, and the error rate from 0.2 to 3 percent. No deploys in the last six hours. The checkout team's dashboards show nothing wrong in their own code.

Opening 20 slow traces, the critical path in 18 of them runs through payments, then fraud-check, where the fraud-check span shows 2 seconds on the payments (client) side but only 40 ms on the fraud-check (server) side. The time is between the services. Splitting fraud-check's client metrics by destination host shows it: 1 of 12 fraud-check hosts has p99 of 2 seconds; the other 11 are normal. That host's ss -ti output shows retransmissions on 4 percent of segments. The cloud provider later confirms a degraded network card.

Meanwhile, the error rate keeps climbing even after the bad host is removed at 14:25: payments retries fraud-check 3 times, and checkout retries payments 2 times, so slow calls produced up to 6 times the normal load on fraud-check, whose queues are now full. The team sheds non-essential traffic (fraud scoring for low-value orders falls back to rules) for 10 minutes, the queues drain, and latency recovers. Follow-ups: retry only at the payments layer, a 10 percent retry budget, outlier detection to eject slow hosts automatically, and tail-based sampling so every slow trace is kept.

In the wild

Trade-offs

TechniqueStrengthsWeaknessesChoose when
Distributed tracingShows the path and timing of one requestInstrumentation and storage costAny system with more than a few services
Split metrics by host, zone, versionReveals gray and partial failuresHigh-cardinality storage costAlways for critical dependencies
Logs with trace IDsDetail per requestVolume, needs searchError investigation
Packet captureGround truth on the networkHeavy, needs expertiseSuspected network problems
Head versus tail samplingHead is cheap; tail keeps all slow requestsTail needs buffering and a collectorTail when slow requests are rare and important

Pitfalls in production

Leadership lens

Check yourself

  1. A client span shows 2 seconds and the matching server span shows 40 ms. Where did the time go?

    Show answer

    Between the two: network delays or retransmissions, connection setup or pool waits, or queueing before the server started processing.

  2. Why can retries at every layer turn a short outage into a long one?

    Show answer

    They multiply load on the failing component exponentially with depth, keeping it overloaded after the original trigger has gone.

  3. Why follow the child that finished last when computing a critical path?

    Show answer

    The parent cannot finish before that child, so it is the one that determines the parent's end time; speeding up earlier-finishing children would not help.

  4. What is a gray failure, and how do you spot it?

    Show answer

    A component that looks healthy to its own checks but is degraded for real work; you spot it by splitting metrics by host, zone or version and comparing callers' views.

Sources and further study

Primary

Books

Other curricula

Free resources

Open in the interactive app: practice, quiz, mock interview →