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 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 componentsContinue learning
Distributed Tracing: See Every Request Across Your Entire System
6 min readopentelemetryOpenTelemetry Instrumentation Guide: Auto vs Manual, SDK Setup & Vendor-Agnostic Observability
5 min readdistributed tracingDistributed Tracing Sampling Strategies: Head-Based, Tail-Based & Beyond
7 min readGo deeper on observability and reliability
As an Amazon Associate I earn from qualifying purchases. Codelit may receive a commission at no extra cost to you.
Site Reliability Engineering
Betsy Beyer, Niall Richard Murphy · 2016
Google's on-call postmortems, error-budget math, and staffing models across 550 pages of practice.
4.7 (1.2k)#1 in Network Disaster & Recovery AdministrationThe Field Guide to Understanding 'Human Error'
Sidney Dekker · 2014
The reasoning behind blameless postmortems, from the safety researcher SRE borrowed it from.
Building Microservices
Sam Newman · 2021
600 pages on splitting a monolith: service boundaries, data decomposition, migration order.
Software Architecture: The Hard Parts
Neal Ford, Mark Richards, Pramod Sadalage, Zhamak Dehghani · 2021
Works through service granularity, distributed data and saga patterns as full trade-off case studies.
4.6 (731)#2 in Software Design Tools