Spans, Traces & Context Propagation
The trace ID that ties everything together, and how it rides along with every request.
To use tracing well, you need its vocabulary. A trace is made of spans, connected by IDs, and the magic that keeps them connected across services is context propagation. Letβs break it down.
π― Learning Objectives
- Define span and trace
- Understand trace IDs, span IDs, and parent-child links
- Understand how trace context propagates between services
- Read the headers that carry that context
Spans and Traces
A span is a single named, timed operation β one unit of work (an HTTP call, a DB query, a function). A trace is the whole tree of spans for one request.
Trace (traceId = abc123)
ββ span: GET /checkout (root span)
ββ span: call orders-service
β ββ span: SELECT order (child)
ββ span: call payments-service
ββ span: POST to bank API (child)Each span records:
- A name (e.g.
GET /checkout) - A start time and duration
- IDs linking it into the trace
- Optional tags (key/value metadata) and annotations (timed events)
The IDs That Connect Everything
Three IDs turn a pile of spans into a tree:
| ID | Meaning |
|---|---|
| traceId | Same for every span in one request β the glue |
| spanId | Unique to this individual span |
| parentId | The spanId of the span that caused this one |
traceId=abc123 spanId=1 parentId=none GET /checkout
traceId=abc123 spanId=2 parentId=1 call orders
traceId=abc123 spanId=3 parentId=2 SELECT order
traceId=abc123 spanId=4 parentId=1 call paymentsBecause they share traceId=abc123, Zipkin knows these spans belong together; parentId tells it the nesting. Thatβs how the waterfall is reconstructed.
Context Propagation β The Key Idea
Hereβs the crucial part: when service A calls service B, it must pass the trace context along, or B starts a brand-new trace and the story breaks. This passing-along is context propagation, and it happens via HTTP headers.
Zipkinβs classic B3 headers carry the context:
GET /pay HTTP/1.1
Host: payments
X-B3-TraceId: abc123
X-B3-SpanId: 4
X-B3-ParentSpanId: 1
X-B3-Sampled: 1Service B reads these, continues the same trace (abc123), and creates its child spans under it. The modern standard, W3C Trace Context, does the same job with a single traceparent header:
traceparent: 00-abc123...-00f067aa0ba902b7-01β Propagation is where tracing usually breaks
If a trace looks broken (each service shows its own separate trace), the cause is almost always lost context propagation β a client that didnβt forward the headers, or an async boundary (queue, thread pool) that dropped them. Forwarding context is the thing to get right.
Sampling β You Donβt Trace Everything
Tracing every request in a high-traffic system is expensive. Sampling decides which requests to record. The X-B3-Sampled: 1 header above means βrecord this one.β Common strategies:
- Rate-based β record 1% (or 10%) of requests.
- Head-based β the decision is made at the start and propagated so a whole trace is consistently recorded or not.
π‘ Keep the decision consistent
The sampling decision is made once at the edge and carried in the context, so either the whole trace is recorded or none of it is β you never get half a trace.
π§ͺ Hands-on Lab
Reconstruct the Tree
Given these spans, draw the parent-child tree and identify the root span:
traceId=x9 spanId=A parentId=none
traceId=x9 spanId=B parentId=A
traceId=x9 spanId=C parentId=A
traceId=x9 spanId=D parentId=Bπ§ Knowledge Check
What links all the spans of a single request together?
Why is context propagation essential for tracing to work across services?
πΌ Interview Preparation
A trace is showing up as several disconnected traces instead of one. What's wrong and how do you fix it?
Summary
You now know the tracing data model β spans, traces, the traceId/spanId/parentId relationship β and the critical role of context propagation and sampling. Next, we actually instrument a service and send real traces to Zipkin.