A trace only stays connected across a network call if the receiving service knows which trace it belongs to. That's the job of context propagation.
The problem: a trace across processes
Inside a single process, the active span is tracked implicitly through the language's context mechanism (thread-locals, async context, etc.) — a child span created anywhere in the call stack automatically picks up the current active span as its parent. But once a request crosses a network boundary — an HTTP call to another service, a message published to a queue — that in-memory context is gone. The receiving process has no idea a trace is even in progress unless the sender explicitly encodes the trace identity into the request and the receiver decodes it.
Context, in-process
OpenTelemetry's Context API is a generic, immutable key-value carrier used to pass the active span (and other cross-cutting data) through a call chain without threading extra parameters through every function signature. When you start a span, it's placed into the current Context; code executing "inside" that context sees it as the active span and automatically parents new spans to it.
Propagators
A Propagator knows how to serialize Context into a wire format (typically HTTP headers) on the outgoing side, and deserialize it back into Context on the incoming side. OpenTelemetry ships two propagators by default, composed together:
- TraceContext propagator — implements the W3C Trace Context spec (
traceparent/tracestateheaders). - Baggage propagator — implements the W3C Baggage spec (
baggageheader).
Instrumentation libraries (HTTP clients, gRPC, message queue clients) call the registered propagator automatically — inject on the client side, extract on the server side — so in most cases you never touch this API directly.
// Outgoing (usually done for you by HTTP instrumentation)
propagation.inject(context.active(), headers);
fetch(url, { headers });
// Incoming (usually done for you by HTTP instrumentation)
const extractedContext = propagation.extract(context.active(), req.headers);
context.with(extractedContext, () => { /* handle request; new spans parent correctly */ });
The W3C traceparent header
The traceparent header is the actual identity that hops between services. Its format is fixed and compact:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
│ └────────────trace-id───────────┘ └───parent-id──┘ │
version trace-flags
- version — currently always
00. - trace-id — 16 bytes (32 hex chars), shared across the whole trace.
- parent-id — 8 bytes (16 hex chars) identifying the span the receiver should parent to.
- trace-flags — currently one bit used:
01means sampled,00means not sampled.
Because this is a W3C standard (not OpenTelemetry-specific), it interoperates with any tracing system that also implements it — including many vendors' proprietary agents.
Baggage
Baggage is a separate mechanism for propagating arbitrary key-value application data alongside the trace — not telemetry itself, but contextual data that downstream services might want to read: a user tier, an A/B test cohort, a tenant ID.
const bag = propagation.createBaggage({ 'user.tier': { value: 'premium' } });
context.with(propagation.setBaggage(context.active(), bag), () => {
// any span created here, or in downstream services after propagation,
// can read propagation.getBaggage(context.active())?.getEntry('user.tier')
});
Baggage propagates through the request chain, but it does not automatically become a span attribute — you must explicitly read it and call span.setAttribute() if you want it visible on spans. It also travels in a plaintext HTTP header on every hop, so never put secrets or PII in baggage.
Common pitfalls
- Async boundaries losing context. Fire-and-forget callbacks, timers, or manually-created threads can lose the active context if the language runtime doesn't propagate it automatically — you may need to explicitly capture and re-bind context.
- Missing propagation across message queues. Unlike HTTP, queue instrumentation must explicitly inject the traceparent into message metadata/headers on publish and extract it on consume — check that your queue's instrumentation library supports this.
- Multiple propagator formats disagreeing. If services in your system use different legacy tracing headers (e.g. B3 from Zipkin) alongside W3C Trace Context, register a composite propagator that understands both, or traces will break at the boundary.
Context propagation is what turns a pile of disconnected per-process spans into one coherent distributed trace. The traceparent header carries trace identity; Baggage carries arbitrary application context alongside it — both travel automatically once instrumentation libraries are in place.