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=01on 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
baggageortracestateshould 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.