Context propagation is the mechanism that carries trace context, such as the trace ID, parent span ID and sampling decision, from one service to the next with each request or message. It is what links spans emitted by different processes into a single distributed trace. The standard format for HTTP is the W3C Trace Context traceparent header.
What context propagation is
A distributed trace is assembled from many spans that are created in different processes, often on different machines and in different languages. Each service only knows about its own work. Context propagation is what tells a downstream service: "this request belongs to trace X, and the span that called you is Y". The downstream service records its spans with the same trace ID and with Y as parent, so the backend can rebuild the complete request tree.
Context has two parts. The span context identifies the trace and the current span and carries flags such as whether the trace is sampled. Baggage is optional, user-defined key-value data, for example a tenant ID, that travels along with the request so that later services can use it.
How it works: inject and extract
Propagation happens in two steps. When a service makes an outgoing call, its instrumentation injects the current context into the carrier, typically HTTP headers, gRPC metadata or message headers. When the receiving service accepts the call, its instrumentation extracts the context from the carrier and makes it the parent of the new server span. In OpenTelemetry, components called propagators perform this work, and the OTEL_PROPAGATORS environment variable selects them. The default is tracecontext,baggage.
Within a process, context must also flow across threads, callbacks and async tasks. Language runtimes handle this differently, for example through thread-local storage in Java or async context tracking in Node.js, and OpenTelemetry's context API abstracts these differences.
The W3C Trace Context standard
Before standardization, every tracing system used its own headers, such as Zipkin's B3 headers or vendor-specific formats, which broke traces at every boundary between tools. The W3C Trace Context Recommendation defines two HTTP headers that all compliant systems understand:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
tracestate: vendorname=opaque-value
| Field | Example | Meaning |
|---|---|---|
| version | 00 | Format version, currently 00 |
| trace-id | 0af7651916cd43dd8448eb211c80319c | 16 bytes as 32 hex characters, identifies the whole trace |
| parent-id | b7ad6b7169203331 | 8 bytes as 16 hex characters, the ID of the calling span |
| trace-flags | 01 | Bit field; 01 means the caller sampled the trace |
The tracestate header carries vendor-specific data alongside, and the separate W3C Baggage specification defines the baggage header for user-defined key-value pairs.
Messaging and asynchronous work
Queues and event streams need propagation too. With Kafka, for example, the producer's instrumentation writes traceparent into the record headers, and the consumer extracts it. Because one consumer batch may contain messages from many traces, OpenTelemetry messaging conventions often use span links rather than a single parent to connect consumer spans to their producers. Without propagation in message headers, every asynchronous hop starts a new, disconnected trace.
Propagating the sampling decision
The sampled flag in traceparent matters for cost and completeness. With head-based sampling, the first service decides whether a trace is recorded, and downstream services honor that decision through a parent-based sampler. If services ignore the flag and sample independently, you end up with fragments of traces instead of complete ones. Tail-based sampling in a collector avoids this problem by deciding after all spans have arrived.
Best practices
- Standardize on W3C Trace Context across all services, proxies and gateways. Configure additional propagators such as B3 only where legacy systems require them.
- Make sure proxies forward headers. Load balancers, API gateways and service meshes must pass
traceparentandtracestateunchanged, or participate in tracing themselves. - Keep baggage small and non-sensitive. Baggage is sent with every downstream request and may leave your trust boundary, so never put credentials or personal data into it.
- Decide how to treat external input. Accepting
traceparentfrom the public internet lets clients influence sampling; many teams start a new trace at the edge and keep the inbound context as a link. - Log the trace ID. Writing
trace_idandspan_idinto structured logs connects logs and traces.
Common mistakes
Broken traces are the typical symptom of a propagation gap: a thread pool that loses context, a homegrown HTTP client without instrumentation, a message bus without header support, or a proxy that strips unknown headers. Mixed propagator configurations are another source, where one service only writes B3 and the next only reads W3C. Finally, generating a new trace ID in every service, for example for logging, defeats the purpose entirely.
How Obseria helps
Obseria is OpenTelemetry-native and uses W3C Trace Context end to end, so traces from OpenTelemetry SDKs and agents connect without proprietary headers. Distributed tracing in Obseria highlights traces with missing parents, which helps you find where propagation breaks. Read more in What is OpenTelemetry? and the Kafka integration.