OpenTelemetry Baggage is the unsung hero of distributed tracing, letting you thread specific, lightweight context through your requests across service boundaries, not just for tracing but for any arbitrary key-value data you need.

Imagine a user makes a request that fans out to several microservices. We want to track which user initiated this entire chain of requests, not just for debugging but perhaps to attribute downstream costs or apply user-specific rate limiting.

Here’s a request flow in action, showing baggage being passed:

Service A (Ingress)

POST /api/v1/process HTTP/1.1
Host: service-a.example.com
Content-Type: application/json
baggage: user-id=user-12345,tenant-id=abcde

{
  "data": "some payload"
}

Service B (Called by A)

Service A receives the request, extracts the user-id and tenant-id from the baggage header, and forwards the request to Service B, carrying the baggage forward.

POST /api/v1/process/intermediate HTTP/1.1
Host: service-b.example.com
Content-Type: application/json
baggage: user-id=user-12345,tenant-id=abcde,service-a-processed=true

{
  "data": "some payload from A",
  "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
}

Notice how Service B receives the original baggage and adds its own service-a-processed=true flag. This is crucial: baggage is additive.

Service C (Called by B)

Service B processes the request and calls Service C. It passes along the baggage it received, plus its own context.

POST /api/v1/process/final HTTP/1.1
Host: service-c.example.com
Content-Type: application/json
baggage: user-id=user-12345,tenant-id=abcde,service-a-processed=true,service-b-processed=true

{
  "data": "some payload from B",
  "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
}

Now, Service C has the full lineage: the original user and tenant, and confirmation that both Service A and Service B participated in this request.

The mental model for baggage is simple: it’s a set of key-value pairs that are propagated with requests. Think of it like a small, encrypted envelope attached to each request. When a service receives a request, it can peek inside this envelope, potentially add its own notes, and then re-seal it before passing it to the next service.

The primary mechanism for baggage propagation is the baggage HTTP header, defined by the W3C Baggage specification. OpenTelemetry SDKs automatically instrument common frameworks (like web servers and HTTP clients) to inject outgoing baggage and extract incoming baggage.

The structure of the baggage header is key1=value1,key2=value2. Importantly, keys and values are URL-encoded if they contain special characters. The values are also limited in size (typically 8KB per header, per OpenTelemetry spec recommendations).

When you configure OpenTelemetry, you define what baggage to create and how to extract it. For instance, in a Java application using Spring Boot, you might have code like this in a filter or interceptor:

// In an incoming request filter
String userId = request.getHeader("X-User-ID"); // Or extract from auth token
if (userId != null) {
    Baggage.current().set("user-id", userId);
}

// In an outgoing HTTP client
// When making a call to another service, the SDK automatically
// serializes the current baggage into the 'baggage' header.

The beauty is that you don’t usually need to manually manage the baggage header itself. The OpenTelemetry SDKs do the heavy lifting. When a service wants to add baggage, it uses the SDK’s API to set a key-value pair on the current baggage context. When a service receives a request with a baggage header, the SDK parses it and makes those key-value pairs available via the SDK’s API.

One thing that often trips people up is understanding that baggage isn’t automatically created from request attributes. You have to explicitly tell the system what you want to track. If Service A receives a JWT token with a sub claim that represents the user ID, it must explicitly extract that sub claim and then Baggage.current().set("user-id", subClaimValue) for it to become part of the baggage. Similarly, if a service needs to add a flag like service-a-processed=true, it must do so explicitly using the SDK’s set operation. The system doesn’t magically know to include this information; you define the policy.

The next logical step after understanding how to pass context is to see how this context can be used for more advanced scenarios like error handling or conditional retries based on upstream service behavior.

Want structured learning?

Take the full Opentelemetry course →