arthrop0d

Brief articles on varied topics.

How W3C Trace Context Propagates Distributed Traces

observabilitydistributed systemsnetworking

W3C Trace Context is a standard way for services to pass distributed-tracing information between one another. Its traceparent HTTP header carries the identity of the overall trace, the current operation, and a sampling decision so separate services can record one connected request path.

The problem it solves

A distributed request rarely stays in one process. An API might call an authentication service, which calls a database proxy, which calls another internal service. Each process can log its own timing, but without shared identity there is no reliable way to tell that those operations belong to the same request.

Before a common standard, tracing libraries used incompatible headers and formats. A proxy might understand one vendor's header while an application used another. Teams then had to configure every boundary carefully, and a service that did not recognise the incoming format could start an unrelated trace.

Trace Context gives cooperating components a small, vendor-neutral piece of context to carry over a boundary such as an HTTP request. It does not itself collect spans or send them to a tracing backend. It tells each component how its local span fits into the larger trace.

A span is a timed record of one operation, such as handling an HTTP request or making a database call. A trace is the complete set of related spans for one distributed operation.

The traceparent value

A valid traceparent header has this form:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

The four fields are separated by hyphens:

Field Size Meaning
Version 2 hexadecimal characters The Trace Context format version; 00 is the current widely used version.
Trace ID 32 hexadecimal characters Identifies the entire distributed trace.
Parent ID 16 hexadecimal characters Identifies the span that created or forwarded this context.
Trace flags 2 hexadecimal characters Control bits, including the sampling decision.

The IDs are hexadecimal values, not arbitrary request names. A trace ID must be globally unique enough that unrelated requests do not collide. A span ID identifies one span within that trace and must be unique within the trace. The all-zero values are invalid.

The final field is commonly 01 or 00. Its least-significant bit is the sampled flag: 1 means the sender recommends that the trace be recorded, and 0 means it recommends that it not be recorded. This is a propagation decision, not a promise that a backend has stored the trace. An implementation may still discard data because of local limits or configuration.

How a request becomes a chain of spans

Suppose a client starts a new operation. Its tracing library creates a trace ID and a new span ID for the client's current span. It sends the current span's ID as the parent-id field:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

When the API receives this header, it creates a server span. The incoming trace-id becomes the server span's trace ID. The incoming parent-id becomes the server span's parent ID. The API then generates a new span ID for its own server span.

That new span is the API's current operation. If the API calls an inventory service, it sends a new traceparent value whose trace ID is unchanged but whose parent ID is now the API span's ID:

# API -> inventory service
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-5c3e7a1b2d4f8091-01

The inventory service repeats the process: it creates a span whose parent is 5c3e7a1b2d4f8091, then generates its own span ID for any downstream call. This creates a parent-child chain without passing the original header unchanged.

The key rule is: keep the trace ID, replace the parent ID with the current span's ID.

This is why a trace can be rendered as a tree. The API span is a child of the client span, and the inventory span is a child of the API span. A service should not reuse an incoming span ID as its own: doing so would merge separate operations and corrupt the trace structure.

Sampling across service boundaries

Tracing every operation can be expensive, especially at high request rates. Sampling means choosing which traces produce the detailed data that will be retained or exported.

The component that starts a trace commonly makes the initial decision. It sets the sampled flag in traceparent, and downstream services propagate that flag when creating their own outgoing context. This lets one request remain consistently selected across services instead of producing a partial trace merely because each service sampled independently.

The flag is a recommendation carried in the standard context. Instrumentation and tracing systems may apply policy locally, and the flag does not force a service to retain data. Conversely, a service can often record useful local diagnostics even when the distributed trace is not sampled. Vendor-specific sampling information may be carried separately in the optional tracestate header; traceparent remains the portable part understood by different implementations.

Missing or invalid context

If a request has no valid traceparent, the receiving service starts a new root trace with a fresh trace ID. If the header is malformed—wrong field lengths, invalid hexadecimal characters, forbidden all-zero IDs, or an unsupported version—the service should not build a parent relationship from it. It can treat the request as a new root, while recording the malformed context for diagnostics according to its security and logging policy.

This mechanism is often visible indirectly: a log line may contain a trace ID and span ID, an incident tool may show a request crossing several services, or a configuration flag may control whether sampled traces are exported. Those systems work because each boundary creates a new span while preserving the trace ID and propagating the current sampling decision through traceparent.

← All articles