Back to HTTP Headers

Tracestate observability both

Carries vendor-specific distributed tracing metadata alongside Traceparent — allows multiple tracing systems to coexist on the same request.

What it does

Tracestate travels alongside Traceparent and carries vendor-specific or system-specific tracing metadata that doesn't fit into Traceparent's fixed format. While Traceparent is the standardised trace ID everyone agrees on, Tracestate is the escape hatch where each vendor can add their own data.

The key design goal: multiple tracing systems can coexist on the same request without clobbering each other. Datadog adds its data, Zipkin adds its data, your custom system adds its data — all in the same Tracestate header, all preserved as the request travels through services.

Syntax

Tracestate: <vendor>=<value>[,<vendor>=<value>]*

A comma-separated list of vendor=value pairs. Each vendor owns their key — no two vendors should use the same key:

Tracestate: dd=s:1;t.dm:-0
Tracestate: vendorname=opaquevalue
Tracestate: dd=s:1;t.dm:-0,rojo=00f067aa0ba902b7
Tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7

Constraints

  • Maximum 32 list members (vendor=value pairs)
  • Each member's key: 1–256 characters, lowercase letters, digits, _, -, *, /
  • Each member's value: 1–256 characters, printable ASCII, no commas or =
  • Total header value: maximum 512 characters
  • Vendor keys are registered or follow the vendorname@systemname format for multi-tenant systems

What vendors put in Tracestate

Datadog:

Tracestate: dd=s:1;t.dm:-0;t.tid:674f4b18000000
  • s:1 — sampling priority (1 = keep)
  • t.dm:-0 — decision maker (-0 = agent)
  • t.tid — high 64-bits of a 128-bit trace ID

Zipkin B3:

Tracestate: b3=0

Simple sampling flag.

Custom/internal:

Tracestate: mycompany=region:us-east-1;deploy:canary

Always propagate, selectively mutate

The cardinal rule: always forward Tracestate unchanged unless you own a specific vendor key.

  • You own mycompany key? You may update mycompany=...
  • You don't own dd or b3? Pass them through verbatim
  • Got no Tracestate incoming? Start a fresh one with your vendor entry if needed

This is how traces survive multi-vendor environments without data loss.

Traceparent and Tracestate together

They're always a pair. Never forward one without the other:

// Propagating both when making outbound calls
Http::withHeaders([
    'Traceparent' => $currentTraceparent,
    'Tracestate'  => $currentTracestate ?? '',
])->post('http://other-service/endpoint', $data);

If an incoming request has Traceparent but no Tracestate, that's valid — pass an empty Tracestate or omit it. Never generate a Tracestate without a corresponding Traceparent.

Mutating Tracestate correctly

When your service adds/updates its vendor entry:

  1. Add your updated entry at the beginning of the list (leftmost = most recent)
  2. Remove your old entry if it was already in the list
  3. If the list would exceed 32 members, drop entries from the end
  4. If your entry would exceed 128 chars, drop it (don't truncate — corrupted data is worse than missing data)
function updateTracestate(string $existing, string $myKey, string $myValue): string
{
    $pairs = array_filter(explode(',', $existing), fn($p) => !str_starts_with(trim($p), "$myKey="));
    array_unshift($pairs, "$myKey=$myValue");
    return implode(',', array_slice($pairs, 0, 32));
}

Common mistakes and gotchas

Stripping Tracestate at service boundaries. A service that forwards Traceparent but drops Tracestate breaks Datadog's sampling pipeline, Zipkin's debug flags, and any other vendor data. Always forward both.

Modifying another vendor's key. Each key is owned by its vendor. If you don't own dd, don't touch it. Mutating another vendor's value corrupts their tracing data silently.

Exceeding the 512-char limit. In long service chains, Tracestate can grow. Each service adds entries; old ones accumulate. The spec says drop from the end when the limit is hit — but if your services aren't implementing this, the header eventually gets dropped entirely by intermediaries.

Real-world examples

Incoming request with Datadog context:

GET /api/users HTTP/1.1
Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Tracestate: dd=s:1;t.dm:-0;t.tid:674f4b18000000

After passing through an internal service that adds its own entry:

Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-bb9952d88cf7f9c5-01
Tracestate: internal=region:eu-west-1,dd=s:1;t.dm:-0;t.tid:674f4b18000000

internal entry prepended; dd entry preserved unchanged; Traceparent has new span-id.

Multi-vendor trace:

Tracestate: b3=1,dd=s:1,custom=abc123

FAQ

Is Tracestate required?

No — Traceparent is required for W3C trace context; Tracestate is optional. A trace without Tracestate works fine. Tracestate only matters when vendor-specific data needs to travel with the trace. If you're using a single observability platform that doesn't use Tracestate, you might never set it.

Can I use Tracestate for my own application data?

Yes, with your own vendor key. Pick a key that's clearly yours (yourcompany, yourapp, etc.) and keep the value concise. Don't abuse it for large payloads — it's for tracing metadata, not arbitrary data transport.

What if I don't care about Tracestate and just want basic tracing?

Implement Traceparent propagation and ignore Tracestate. Your traces will work across services. You'll lose vendor-specific features (Datadog's sampling decisions traveling with the trace, etc.), but the basic trace stitching will work perfectly. Forward whatever Tracestate arrives to avoid breaking anything, even if you never read or write it yourself.

Fun fact

The Tracestate design — a shared header where multiple vendors each own a key — solved a political problem as much as a technical one. The W3C working group trying to standardise trace context in 2016–2020 included engineers from Google, Microsoft, Dynatrace, Elastic, Datadog, and Lightstep, all of whom had existing proprietary tracing headers. Getting everyone to adopt a single standard required a way for each vendor to keep their own metadata. Tracestate was the compromise: standardise the core (Traceparent) while giving everyone a namespace to carry their proprietary data alongside it. Classic standards-body diplomacy encoded in an HTTP header.