Trace Context Propagation — W3C Headers, B3, Baggage, and Cross-Service Correlation
Why Context Propagation Matters#
A single user request touches 10-20 services in a modern architecture. Without propagation, each service logs independently and you cannot reconstruct what happened end-to-end. Context propagation is the mechanism that threads a single trace ID through every service, queue, and async boundary so you can follow one request across your entire system.
W3C Trace Context Standard#
The W3C Trace Context specification defines two HTTP headers that every service must forward.
traceparent#
The primary header carrying trace identity:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
| | | |
version trace-id parent-id flags
- version: Always
00for the current spec - trace-id: 16-byte hex, globally unique per trace
- parent-id: 8-byte hex, unique per span (changes at each hop)
- flags:
01means sampled,00means not sampled
tracestate#
Vendor-specific data that rides alongside the trace:
tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE
Each vendor gets a key-value pair. Services must propagate all entries, even from unknown vendors. Ordering matters: the leftmost entry belongs to the service that last updated the header.
How Services Propagate#
Service A (generates trace):
traceparent: 00-TRACE_ID-SPAN_A-01
Service A → Service B (HTTP call):
traceparent: 00-TRACE_ID-SPAN_B-01
(trace-id stays the same, parent-id changes to B's span)
Service B → Service C:
traceparent: 00-TRACE_ID-SPAN_C-01
(same trace-id, new parent-id again)
The trace-id never changes. The parent-id updates at each hop to reflect the current span.
B3 Propagation (Zipkin)#
Before W3C standardized trace context, Zipkin's B3 format was the de facto standard. Many systems still use it.
Multi-header Format#
X-B3-TraceId: 463ac35c9f6413ad48485a3953bb6124
X-B3-SpanId: 0020000000000001
X-B3-ParentSpanId: 0010000000000001
X-B3-Sampled: 1
Single-header Format (b3)#
b3: 463ac35c9f6413ad48485a3953bb6124-0020000000000001-1-0010000000000001
{TraceId}-{SpanId}-{SamplingState}-{ParentSpanId}
W3C vs B3#
| Feature | W3C Trace Context | B3 |
|---|---|---|
| Standard | W3C Recommendation | Zipkin community |
| Headers | 2 (traceparent, tracestate) | 4 (or 1 combined) |
| Vendor data | tracestate key-value pairs | Not supported |
| Trace ID size | 128-bit | 64 or 128-bit |
| Adoption | OpenTelemetry default | Zipkin, older systems |
Recommendation: Use W3C for new systems. Support both during migration with OpenTelemetry's composite propagator.
Baggage — Application-Level Context#
Baggage carries arbitrary key-value pairs across service boundaries alongside trace context. It is not trace metadata; it is application data.
baggage: userId=abc123,region=us-east-1,featureFlag=new-checkout
Use Cases#
- User ID: Correlate all spans for a single user session
- Region: Route decisions across services
- Feature flags: Propagate experiment assignments
- Tenant ID: Multi-tenant isolation tracking
Risks#
Baggage is sent with every request. Keep it small:
- Do not store large payloads (serialize to a lookup key instead)
- Limit total baggage size (OpenTelemetry defaults to 8192 bytes)
- Sensitive data in baggage is visible to every downstream service
- Use baggage for metadata, not business logic
Propagation Across Async Boundaries#
HTTP-to-HTTP propagation is straightforward: inject headers on outgoing requests, extract on incoming. Async boundaries require explicit handling.
Message Queues#
Trace context must be embedded in message metadata, not the message body.
Kafka:
Producer:
message.headers["traceparent"] = current_span.traceparent
message.headers["tracestate"] = current_span.tracestate
Consumer:
context = extract(message.headers)
with tracer.start_span("process_message", context=context):
handle(message)
RabbitMQ:
Producer:
properties.headers["traceparent"] = span.traceparent
Consumer:
context = extract(properties.headers)
SQS:
SQS has a 10-attribute limit on message attributes. Use a single traceparent attribute or embed in the message body as a last resort.
Background Jobs#
Workers that pick up jobs from a database or Redis queue:
Enqueue:
job.metadata["traceparent"] = current_context.traceparent
job.metadata["baggage"] = current_context.baggage
Worker:
parent_context = extract(job.metadata)
with tracer.start_span("job.process", context=parent_context, kind=CONSUMER):
execute(job)
Cron Jobs and Scheduled Tasks#
No incoming context exists. Create a new root span:
with tracer.start_span("cron.daily_report", kind=INTERNAL) as span:
span.set_attribute("cron.schedule", "0 2 * * *")
run_report()
Cross-Service Correlation Patterns#
Correlation ID vs Trace ID#
- Trace ID: Scoped to a single distributed trace (one user action)
- Correlation ID: Business-level grouping (order ID, session ID)
Use both. Trace ID for debugging latency. Correlation ID for business context.
baggage: correlationId=order-789,traceId handled by traceparent
Fan-Out Correlation#
When one request triggers multiple parallel downstream calls:
API Gateway (Span A, Trace T1)
├── User Service (Span B, Trace T1, Parent A)
├── Product Service (Span C, Trace T1, Parent A)
└── Pricing Service (Span D, Trace T1, Parent A)
All spans share Trace T1. Parent-id links them back to the gateway span. Tracing backends reconstruct the tree.
Trace Linking Across Systems#
When context cannot be propagated directly (third-party APIs, legacy systems):
Span A (your system):
span.add_link(external_trace_context)
span.set_attribute("external.order_id", "ORD-456")
Span links create a reference without a parent-child relationship. Use them for batch jobs that process items from different traces.
Propagator Configuration in OpenTelemetry#
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.propagators.b3 import B3MultiFormat
from opentelemetry.propagate import set_global_textmap
from opentelemetry.propagators.textmap import TraceContextTextMapPropagator
from opentelemetry.baggage.propagation import W3CBaggagePropagator
# Support W3C, B3, and Baggage simultaneously
set_global_textmap(CompositePropagator([
TraceContextTextMapPropagator(),
W3CBaggagePropagator(),
B3MultiFormat(),
]))
This lets your services communicate with both W3C-compliant and Zipkin-based systems during migration.
Common Propagation Failures#
Context not forwarded: HTTP client libraries must be instrumented. Bare fetch() or requests.get() calls will not propagate headers automatically.
Queue consumers drop context: If your consumer framework does not extract trace headers, every message starts a new trace.
Load balancer strips headers: Verify that your load balancer forwards traceparent and tracestate. Some WAFs strip unknown headers.
Thread pool breaks context: When work is submitted to a thread pool, the trace context from the submitting thread must be explicitly captured and restored in the worker thread.
Sampling mismatch: If Service A samples at 10% and Service B samples at 100%, partial traces appear. Use head-based sampling at the entry point and propagate the sampling decision.
Summary#
- W3C Trace Context is the standard. Use
traceparentandtracestateheaders. - B3 is still common. Support both with a composite propagator.
- Baggage propagates application context (user ID, feature flags) but keep it small.
- Queue propagation requires explicit header injection in message metadata.
- Async boundaries (background jobs, cron) need manual context extraction or new root spans.
- Correlation IDs complement trace IDs for business-level grouping.
Article #437 in the Codelit engineering series. Explore our full library of system design, infrastructure, and architecture guides at codelit.io.
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
Try these templates
Scalable SaaS Application
Modern SaaS with microservices, event-driven processing, and multi-tenant architecture.
10 componentsLogging & Observability Platform
Datadog-like platform with log aggregation, metrics collection, distributed tracing, and alerting.
8 componentsMicroservices with API Gateway
Microservices architecture with API gateway, service discovery, circuit breakers, and distributed tracing.
10 componentsBuild this architecture
Generate an interactive architecture for Trace Context Propagation in seconds.
Try it in Codelit →
Comments