W3C Trace Context explained

One small HTTP header is what ties services together into a single trace. Here is what each field means, how to check it on the wire, and where it usually goes missing.

The header, field by field

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
             |  |                                |                |
             |  trace-id (16 bytes, 32 hex)      |                trace-flags (1 byte)
             version (1 byte)                    parent-id (8 bytes, 16 hex)
Field Example Meaning Validity rule
version 00 Format version ff is invalid
trace-id 4bf92f35...4736 Identifies the whole trace; shared by all spans All zeros is invalid
parent-id 00f067aa0ba902b7 The span ID of the caller's current span All zeros is invalid
trace-flags 01 Bit field; the lowest bit is sampled Other bits must be handled as defined by the version

All hex must be lowercase. The specification tells a receiver to ignore a traceparent it can't parse and start a new trace, so uppercase hex or a truncated ID doesn't produce an error. You simply get a new trace, which is why bad propagation so often goes unnoticed.

Trace Context is a W3C Recommendation. A Level 2 version adds a random flag, indicating that the trace ID was generated with enough randomness to be usable for sampling decisions. Check the spec's status if you depend on it. OpenTelemetry's default propagators are tracecontext and baggage, so anything using the OTel SDK with default settings speaks this format already.

How one call looks

Assume A calls B, and B calls C. The trace ID stays the same, while the parent ID changes at each hop.

A -> B   traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-aaaaaaaaaaaaaaaa-01
         (aaaaaaaaaaaaaaaa is A's client span)

B -> C   traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-bbbbbbbbbbbbbbbb-01
         (bbbbbbbbbbbbbbbb is B's client span)

B has created two spans: a server span whose parent is aaaa..., and a client span under it, bbbb..., which it sends to C. If you ever see the same parent ID going out from a service that came in, that service is forwarding the header without participating in the trace. That's legal, and it's what an uninstrumented proxy does, but the trace will skip that service. For how to read the result, see what is distributed tracing.

The sampled flag and its effect

The lowest bit of trace-flags says whether the caller recorded this trace. With OpenTelemetry's parentbased_* samplers, a child follows its parent's decision, which keeps the whole trace consistent. If a service uses a plain traceidratio sampler instead, it decides for itself, ignoring the flag, and you may get traces with holes in them.

The trace ID is still propagated when the flag is 00. Your logs can carry the trace ID even for unsampled requests, though there will be no stored trace to jump to. Tail-based samplers typically want everything sent to them, deciding later, so head decisions and tail samplers have to be coordinated. See the tail-based sampling guide.

tracestate: the second header

tracestate: mycompany=t61rcWkgMzE,othersystem=00f067aa0ba902b7

tracestate carries vendor- or system-specific data alongside traceparent, in a comma-separated list of key=value pairs. The spec allows up to 32 list members. A system modifies only its own entry, and moves it to the front when it does. Unknown entries must be passed along.

Most teams never need to touch this header. Don't use it for application data, as that is what baggage is for. If a downstream system complains about header sizes, look here. The spec says that vendors may truncate entries when the header grows large.

Baggage is separate

The baggage header is a different W3C specification. It carries application key-value pairs, for example tenant=emea, so downstream services can read them.

baggage: tenant=emea,request.channel=mobile

Two warnings. It's sent on every outgoing call, including, if you aren't careful, calls to third parties, so it should never contain anything sensitive. And baggage entries aren't span attributes by default, so you need to copy them explicitly if you want them in your traces.

Debug it: check the wire

Go from outside in. First, send a request with a known header and watch what comes out.

TRACE=$(python3 -c 'import secrets; print(secrets.token_hex(16))')
SPAN=$(python3 -c 'import secrets; print(secrets.token_hex(8))')
curl -v -H "traceparent: 00-${TRACE}-${SPAN}-01" https://orders.example.internal/orders/42
echo "search your tracing backend for trace ${TRACE}"

If the trace exists and the first server span has SPAN as its parent, then the entry point works. For a closer look without any instrumented backend, a tiny server that echoes headers is enough to tell you what a proxy passes on:

from http.server import BaseHTTPRequestHandler, HTTPServer


class EchoHeaders(BaseHTTPRequestHandler):
    def do_GET(self):
        body = str(self.headers).encode()
        self.send_response(200)
        self.end_headers()
        self.wfile.write(body)


HTTPServer(("", 8080), EchoHeaders).serve_forever()

Put this behind the same proxy route and look at the result. If traceparent doesn't appear, that hop is the problem. If it appears with your exact value, then the proxy forwards without participating, which you may or may not want.

Where propagation breaks

Location What goes wrong Fix
API gateway or load balancer Header dropped by an allow-list, or a proxy starts a new trace Allow traceparent, tracestate and baggage; decide on participation
Different formats One side uses an older header format, the other W3C Configure both formats in OTEL_PROPAGATORS during the migration, for example tracecontext,baggage plus the older one
Thread pools Work is handed to a worker thread and the current context is lost Wrap executors with context-aware versions; auto-instrumentation handles common ones
Async and reactive code Context doesn't follow callbacks Use the language's context tools; Python contextvars, Node.js AsyncLocalStorage
Message brokers No HTTP header to carry context Inject into message headers; consumers extract them
Hand-written HTTP clients A custom wrapper builds a new header set Use the propagator's inject on outgoing headers
Legacy middleware Unknown headers stripped Allow them, or accept a gap in the trace
Service mesh Sidecars create spans but apps must still copy the context between incoming and outgoing calls Instrument the app; the sidecar can't link the two calls alone

That last row catches many people out. A proxy sees an inbound request and an outbound request, but it can't tell which of them belong together. Only the application knows that, so the application has to propagate the context.

Manual propagation when you have to

If a library isn't covered by auto-instrumentation, use the propagation API instead of building the string yourself:

from opentelemetry import propagate, trace

tracer = trace.get_tracer("jobs.publisher")


def publish(broker, topic, payload):
    with tracer.start_as_current_span("publish", kind=trace.SpanKind.PRODUCER):
        headers = {}
        propagate.inject(headers)
        broker.send(topic, payload, headers=headers)


def consume(message):
    context = propagate.extract(message.headers)
    with tracer.start_as_current_span("process", context=context, kind=trace.SpanKind.CONSUMER):
        handle(message.payload)

inject uses whatever propagators are configured, so this code keeps working if you change formats. For batches, one consumer span may relate to many producers. Span links are a better fit there than a single parent.

Browsers and CORS

To link a browser fetch with its backend trace, your front end sends traceparent too. On a cross-origin request, a custom header triggers a CORS preflight, so the API must explicitly allow it:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Headers: traceparent, tracestate, content-type

If this is missing, the browser blocks the request, turning a monitoring change into an outage, so roll it out with care. Never send traceparent to third-party origins; only add it for your own API hosts. Those requests can then fail CORS, and you would also be telling that third party your trace IDs.

Trust boundaries

A traceparent arriving from the public internet is client-controlled input:

  • A caller can set sampled=01 on every request and force you to record all of it.
  • A caller can reuse a fixed trace ID, merging unrelated requests into one giant trace.
  • Anything in baggage or tracestate should be treated as untrusted.

At your public edge, consider starting a fresh trace and keeping the external ID as a span link or attribute, rather than continuing it blindly. The spec also suggests caution around privacy: trace IDs generated from personal data, or those that persist across a user's sessions, can act as identifiers. Use random IDs and don't reuse them. Whether your own front end's traceparent should be trusted at the edge is a decision you should make deliberately.

How Wrentrix uses it

In Wrentrix, the browser collector links every fetch to its backend trace through traceparent, so a slow page interaction can be followed into the service that caused it. Per-app keys and allowed-origin lists control what the collector accepts. The real user monitoring page describes that side. On the backend, it accepts traces from any OpenTelemetry SDK, which handles the header for you.

Frequently asked questions

What is the traceparent header?

It is a W3C-standard HTTP header that carries a trace ID, the caller's span ID, a version and flags. Each instrumented service reads it, makes its own span a child and sends an updated value onward. That is what connects one request's spans across services.

What is the difference between traceparent and tracestate?

traceparent holds the fixed, standard identifiers that every system understands. tracestate is an optional list of system-specific entries that travel alongside it. Most applications only need traceparent, while tracing systems use tracestate for their own internal data.

What does the 01 at the end of traceparent mean?

It is the trace-flags byte, where the lowest bit is the sampled flag. A value of 01 means the caller is recording this trace, and 00 means it is not. Downstream services using a parent-based sampler follow that decision.

Does my service mesh handle trace propagation for me?

Not completely. Sidecars can generate spans and add headers, but they can't know which inbound request caused which outbound call. Your application has to copy the context across, which auto-instrumentation or an OpenTelemetry SDK usually does for you.

Can I use W3C Trace Context with message queues?

Yes, by writing the values to message headers or attributes on publish and reading them on consume. W3C has also been working on bindings for some protocols, and OpenTelemetry instrumentation handles many messaging clients. For batch consumption, span links usually model the relationship better than a single parent.