What Is W3C Trace Context?
Learn how W3C Trace Context uses the traceparent and tracestate headers to keep one trace intact across services and vendors.
GrowthX10 min read

W3C Trace Context is the W3C Recommendation that defines two HTTP headers, traceparent and tracestate, so tracing information propagates across distributed systems no matter which tracing tool each service reports to. When every hop forwards those headers, a checkout request can cross several services on different application performance monitoring vendors and still appear as one connected waterfall in your trace viewer.
This guide covers the standard's header fields, its propagation rules, and the OpenTelemetry (OTel) implementation pitfalls that break otherwise compliant traces.
Why Did Distributed Tracing Need a Standard?
Distributed tracing needed a standard because each tracing vendor invented its own header to carry the shared trace identifier. A distributed trace follows one request across every service, queue, and database it touches by stamping each resulting span with that identifier. Zipkin used the B3 header set (X-B3-TraceId, X-B3-SpanId, X-B3-Sampled), Jaeger used its uber-trace-id header, and cloud tracing services used their own headers, such as X-Amzn-Trace-Id and X-Cloud-Trace-Context. The incompatible formats caused four failures:
- No shared identifier: Traces collected by different vendors couldn't be correlated.
- Broken propagation: Traces crossing a vendor boundary couldn't be propagated.
- Dropped metadata: Intermediaries could drop vendor-specific metadata.
- No cloud commitment: Cloud providers couldn't promise propagation support without a standard.
The resulting standard "defines a universally agreed-upon format for the exchange of trace context propagation data" and splits it into two fields, traceparent for universal identity and tracestate for vendor-specific data that rides alongside it. Compliant tracing tools "MUST propagate the traceparent and tracestate headers and guarantee traces are not broken." A header format owned by a standards body fits a portable observability stack, where changing tracing backends doesn't mean reinstrumenting every service.
What Does the traceparent Header Contain?
The traceparent header is mandatory, fixed-length, and describes "the position of the incoming request in its trace graph in a portable, fixed-length format," a design that keeps parsing fast. Its value follows the pattern version-trace_id-parent_id-trace_flags, four lowercase hexadecimal fields separated by hyphens. The canonical example from the specification is 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01, and the same value ending in -00 describes an unsampled trace. Every compliant tracing tool must set this header, even one that relies only on data in tracestate.
How the Four traceparent Fields Break Down
traceparent divides trace identity and sampling state into four fixed-size fields:
| Field | Size | Meaning | Invalid values |
|---|---|---|---|
version | 1 byte (2 hex chars) | Format version; currently 00 | ff |
trace-id | 16 bytes (32 hex chars) | "the ID of the whole trace forest," shared by every span in the trace | All zeros |
parent-id | 8 bytes (16 hex chars) | The calling span's ID, "known as the span-id" in some tracing systems | All zeros |
trace-flags | 1 byte (8 bits, 2 hex chars) | Bit field; 01 sets the sampled flag | None specified |
The field sizes come from the Level 2 grammar, and a trace-id or parent-id that fails validation invalidates the whole header. Vendors "MUST ignore the traceparent" when either field contains non-allowed characters or all zeros, and an ignored header means a new trace starts at that service.
Generation guidance sits in non-normative section 8. The trace-id "SHOULD be globally unique" because high-throughput cloud services make local uniqueness insufficient. Randomly generated values "SHOULD be preferred" because randomness lets a vendor base sampling decisions on the trace-id without propagating a separate sampling context.
Under Trace Context Level 2, implementers should randomly generate at least the right-most seven bytes of the trace-id and set a new random trace-id flag (bit 02 in trace-flags) when they do. Section 9 adds that span-id values SHOULD be unique within a trace and randomly generated, so parent-child relationships between spans don't become ambiguous.
What Does the tracestate Header Carry?
The optional tracestate header carries the vendor- or system-specific data that traceparent deliberately leaves out. Its value is a comma-separated list of key=value pairs, with up to 32 members and values up to 256 characters each. The specification's running example shows a Congo-instrumented client sending tracestate: congo=t61rcWkgMzE and a Rojo-instrumented server adding its own entry on the left to produce rojo=00f067aa0ba902b7,congo=t61rcWkgMzE.
Section 3.5 requires a system that receives tracestate to send it on outgoing requests. The system must preserve the order of entries it didn't modify and shouldn't delete keys it didn't generate, because deleting an unknown pair can break correlation in other systems. Each vendor gets one entry per key, and when a request re-enters its system, the vendor overwrites that entry and moves it to the left of the list.
If the sender didn't change traceparent before propagation, it must not change tracestate either. A receiver that can't parse traceparent must not attempt to parse tracestate at all. Every system applying these propagation rules must also keep personally identifiable information out of the header.
How Does Trace Context Propagation Work?
Trace context propagation works by having each service read the incoming headers, continue the trace, and write updated headers on every outbound call. Sections 3.4 and 3.5 contain the binding rules that section 4 explains, and they cover three cases:
- No
traceparentarrives: The service "creates a newtrace-idandparent-idthat represents the current request," becomes the root span of a new trace, and sets freshtraceparentandtracestateheaders on its outbound calls. A tracestate received without a traceparent "is invalid and MUST be discarded," and the sampled flag defaults to 0 when this component initiates the trace. - A valid traceparent arrives, and the service calls downstream: The service keeps the trace-id, replaces parent-id with the ID of its own current span, and may flip the sampled flag, but only together with that parent-id update. Updating parent-id is the default mutation, and vendors "MUST NOT make any other mutations to the traceparent header" apart from a full trace restart or a version downgrade.
- The request leaves HTTP for a queue or async boundary: The core standard defines only the HTTP binding, and Amazon SQS and Advanced Message Queuing Protocol (AMQP) are non-HTTP transports. For Kafka records, SQS messages, or RabbitMQ publishes, the same two values ride in message headers or attributes instead. A producer "SHOULD attach a message creation context to each message," because consumer traces can't be correlated with producer traces otherwise, and the W3C bindings for message protocols such as AMQP remain internal drafts without standards status.
The first two cases happen automatically wherever your framework has an instrumentation library, and the third depends on whether the broker client does. Manual propagation is rare, but a custom protocol between services or a library with no instrumentation can require it. In that case, you inject the context into whatever metadata field the transport offers and extract it on the receiving side.
In Coralogix, a cross-stack observability platform, APM runs on an OpenTelemetry-native architecture, so traces from those instrumentation libraries arrive through OpenTelemetry-native ingestion without reinstrumenting your services.

How OpenTelemetry Uses W3C Trace Context
OpenTelemetry made W3C Trace Context its default wire format. The OTel Propagators API requires the W3C TraceContext propagator to "parse and validate the traceparent and tracestate HTTP headers as specified in W3C Trace Context Level 2."
The SDK configuration specification sets OTEL_PROPAGATORS to tracecontext,baggage by default, marks the jaeger and ottrace propagators deprecated, and leaves b3 and xray as non-default options. With those defaults, a Go gateway and a Python service on OTel SDKs inject and extract the same two headers as a Java consumer, without any team writing header code.
OTel already targets Trace Context Level 2, which became a Candidate Recommendation Draft on March 28, 2024, and has not yet become a Recommendation. All OTel SDKs plan to set the Level 2 random flag, and because the traceparent layout itself is unchanged, version 00 implementations keep working.
How to Move from B3 or Jaeger Headers to traceparent
Moving from B3 or Jaeger headers to traceparent keeps existing traces intact when every service can read both formats during the rollout. The legacy formats carry the same three facts as traceparent, a trace ID, the caller's span ID, and a sampling decision, in different shapes:
| Format | Header | Trace ID | Caller's span ID | Sampling decision |
|---|---|---|---|---|
| W3C Trace Context | traceparent (plus tracestate) | 32 lowercase hex characters | parent-id, 16 hex characters | trace-flags bit 01 |
| B3 | X-B3-TraceId, X-B3-SpanId, X-B3-ParentSpanId, X-B3-Sampled, or the single b3 header | 16 or 32 lowercase hex characters | X-B3-SpanId, 16 hex characters | X-B3-Sampled: 1 or 0 |
| Jaeger | uber-trace-id as {trace-id}:{span-id}:{parent-span-id}:{flags} | 64-bit or 128-bit, in hex | span-id field | flags bit 0x01 |
| AWS tracing header | X-Amzn-Trace-Id as Root=…;Parent=…;Sampled=… | 1-, eight hex digits of epoch time, then a 96-bit ID in 24 hex digits | Parent segment ID | Sampled=1 |
In OTel, setting OTEL_PROPAGATORS to a list such as tracecontext,baggage,b3 builds a composite propagator, which injects every listed format on outbound calls and calls each one in order when extracting incoming context. Roll that setting out to every service in the request path first, because a service that still emits only X-B3-TraceId or uber-trace-id would otherwise start a new trace at any downstream neighbor that reads only traceparent.
Remove the legacy propagator once no service emits only the old format. Jaeger's own SDK migration guide already marks its wire format deprecated and recommends W3C Trace Context, so new instrumentation should start on traceparent.
What Breaks Otherwise Compliant Traces?
Otherwise compliant traces break or mislead when a hop drops a header, an ID generator loses its randomness, an edge service trusts outside input, or a broker boundary loses the context. Four of the five failures below trace back to the rules in section 3.5 or the guidance in sections 4.2 and 8:
- Forwarding
traceparentbut droppingtracestate: Section 3.5 requires services to send a receivedtracestateon outgoing requests, yet one Terragrunt issue report found that onlyTRACEPARENTreached child processes, so vendor sampling state didn't survive the hop. The trace still stitches, but the vendor metadata disappears without an error. - Seeding the ID generator: The OpenTelemetry Python SDK's default ID generator didn't resist random.seed() calls in user code, which can produce duplicate trace IDs and undermines the randomness section 8 prefers.
- Letting a service become a false root: When a proxy or client strips the header, section 4.2 applies and the next service mints a new
trace-id. For example, an NGINX proxy that doesn't forward the context to a Node.js application produces two traces instead of one. - Trusting
traceparentat a public edge: An edge service that accepts traceparent from the internet lets any client choose its own trace-id and sampled flag, which can stitch unrelated requests into one trace or push an outside sampling decision downstream. Edge services should decide deliberately whether to continue an incoming trace or restart it. - Losing context at the broker: A producer that publishes to Kafka or SQS without injecting context, or a consumer that doesn't extract it, starts a fresh trace on the consumer side. In one OpenTelemetry Java issue, SQS traces broke across languages because Java services propagated through an AWS-specific trace header while Node.js and Ruby services used message attributes.
Aside from dropped trace-state and public-edge cases, each failure shows up as a span with no parent or a trace that ends a hop early. Root cause analysis starts with confirming both headers reached every service in the path, checking each proxy and thread boundary first, then each producer, until you isolate the component that failed to inject or forward the context.
What W3C Trace Context Means for Your Observability Stack
W3C Trace Context keeps a trace intact only when every hop forwards both headers, with traceparent carrying the trace-id, parent-id, and trace-flags and tracestate carrying vendor metadata. Coralogix APM ingests those traces natively alongside your logs and metrics, so you can inspect a connected trace after it crosses proxies, process boundaries, and message brokers.
Explore the Coralogix platform to see how far each traceparent travels through your services.

Frequently Asked Questions About W3C Trace Context
What is the difference between tracing and distributed tracing?
Tracing records the execution path of a request inside a single process or service. Distributed tracing extends that across services and networks, including multiple cloud providers, and stitches individual spans together into one trace through a shared trace-id. W3C Trace Context is the standard that makes the stitching work without every service running the same vendor.
How is W3C Baggage different from tracestate?
W3C Baggage uses its own baggage header to carry application-defined key-value pairs, such as a tenant ID, that services read for their own logic. The tracestate header carries tracing vendors' internal state and isn't meant for application data. OTel propagates both by default, so keep sensitive values out of either header.
What is a trace in an API?
In an API, a trace is the complete record of one request from entry through every service and database call, including any external API it touches, until it returns a response. Each step is a span, and all spans with the same trace-id assemble into the trace. The traceparent header lets an API gateway and its downstream microservices share and continue one trace.
Does the sampled flag force downstream services to record spans?
No, the specification calls trace flags recommendations from the caller rather than strict rules. A set sampled flag means the caller may have recorded trace data, and a component that has to make a recording decision SHOULD respect it. Parent-based samplers in OTel follow that guidance, which keeps a trace's sampling decision consistent across services.