September 20, 2026
Your Trace Baggage Is an Outbound Data Channel
OpenTelemetry baggage is useful precisely because it travels. That is also what makes it a trust-boundary problem.

By Arjun Garg
4 min read
A header named baggage does not look especially dangerous. It sounds operational—something attached to a trace so that engineers can make sense of a request as it moves through a system.
But baggage is not limited to tracing metadata. It is a collection of application-defined key-value pairs associated with a distributed request or workflow. When a configured propagator injects that context into an outbound carrier, those values can become an HTTP header on the next request.
baggage: tenant.tier=enterprise,release.ring=canary,user.email=alex%40example.combaggage: tenant.tier=enterprise,release.ring=canary,user.email=alex%40example.comThat is useful inside a controlled environment. It can help services make consistent routing, experimentation, or diagnostic decisions without each service rebuilding the same context.
It also means that a routine call to a fraud provider, shipping carrier, feature-flag service, or support platform can disclose more than the request body says — unless propagation changes at the boundary.
Baggage should be treated as outbound application data, not as harmless exhaust from the observability stack.
Baggage and trace context solve different problems
The distinction matters. Trace context carries the identifiers and options needed to connect spans across processes. Baggage carries properties chosen by the application. The W3C Baggage specification deliberately defines it independently of Trace Context.
That independence is a feature. A service can propagate baggage whether or not it records spans. The OpenTelemetry API is also designed so baggage propagation can function without a full SDK installed.
None of that gives a baggage entry authority or truth. A value arriving as tenant.tier=premium is merely input. If downstream code trusts it for pricing, authorization, routing, or entitlements, the application has silently converted a context hint into a security decision.
There are therefore two directions to review:
- Incoming propagation: Can an untrusted caller inject or overwrite context that affects behavior?
- Outgoing propagation: Can internal context travel to a destination that should never receive it?
OpenTelemetry's current context-propagation guidance calls out both risks: sanitize or ignore untrusted incoming context, and be deliberate about context sent to external or public-facing endpoints. Its baggage guidance specifically warns against credentials, API keys, and personally identifiable information because baggage might be logged or delivered to an untrusted downstream service.
The boundary, not the key name, decides what is safe
A key can be harmless on one route and revealing on another. release.ring=canary may be ordinary internal context, but it can expose deployment strategy to an outside provider. An opaque tenant identifier may not contain a customer's name, yet it can still be linkable across requests. A feature assignment can disclose an unreleased product decision.
This is why a global list of "non-sensitive baggage keys" is too coarse. The better question is:
Which keys may be disclosed to this class of destination for this purpose?
That produces a small, useful boundary model:
- Internal: Context may propagate among services under the same operational and data-governance controls.
- Partner: Only values covered by an explicit integration contract should cross.
- Public or unknown: Start with empty baggage.
The safe default is worth stating plainly: external requests should inherit no baggage unless a field has been approved for that destination.
A boundary wrapper is more reliable than scattered cleanup
Removing sensitive entries wherever developers happen to remember is a fragile control. The policy belongs at the point where the application classifies and sends outbound traffic.
The exact APIs vary by language and OpenTelemetry implementation, but the shape can remain simple:
// Conceptual TypeScript-like pseudocode
const allowedBaggage = {
internal: new Set(["tenant.tier", "release.ring", "support.case_ref"]),
partner: new Set(["support.case_ref"]),
public: new Set()
};
function contextFor(destination, currentContext) {
const trustClass = classifyDestination(destination);
const allowed = allowedBaggage[trustClass];
const clean = emptyBaggage();
for (const [key, value] of baggageEntries(currentContext)) {
if (allowed.has(key)) clean.set(key, value);
}
return withBaggage(currentContext, clean);
}
async function outbound(request, currentContext) {
const safeContext = contextFor(request.url, currentContext);
return instrumentedFetch(request, { context: safeContext });
}// Conceptual TypeScript-like pseudocode
const allowedBaggage = {
internal: new Set(["tenant.tier", "release.ring", "support.case_ref"]),
partner: new Set(["support.case_ref"]),
public: new Set()
};
function contextFor(destination, currentContext) {
const trustClass = classifyDestination(destination);
const allowed = allowedBaggage[trustClass];
const clean = emptyBaggage();
for (const [key, value] of baggageEntries(currentContext)) {
if (allowed.has(key)) clean.set(key, value);
}
return withBaggage(currentContext, clean);
}
async function outbound(request, currentContext) {
const safeContext = contextFor(request.url, currentContext);
return instrumentedFetch(request, { context: safeContext });
}The important decisions are architectural, not syntactic:
- The destination is classified before propagation.
- The policy is an allowlist, so new internal keys do not automatically become externally visible.
- The cleaned baggage replaces inherited baggage; it is not merely appended to it.
- The request still receives trace context if the organization chooses to propagate it. Baggage policy and trace-continuity policy do not have to be identical.
Five controls that survive contact with a real system
1. Give every propagated key an owner
Each approved key needs a purpose, owner, classification, allowed destinations, retention expectation, and deletion condition. Otherwise baggage becomes a distributed junk drawer: easy to add to, difficult to reason about, and nearly impossible to retire confidently.
2. Keep sensitive state behind an opaque reference
If a downstream internal service needs rich customer or transaction context, consider propagating a short-lived reference and resolving the data server-side. This does not remove the need to protect the reference, but it reduces the amount of meaning exposed in transit and makes revocation and auditing more practical.
3. Sanitize incoming baggage before using it
Assume externally supplied baggage can be forged. If a value influences security or money movement, verify it against an authoritative source or use a protected credential designed for that decision. Do not promote a convenient header into an authorization mechanism.
4. Test the emitted carrier, not only the context object
A unit test that inspects an in-memory baggage collection can miss what an HTTP client, proxy, or auto-instrumentation layer actually sends. Add boundary tests that capture the final outbound headers for representative internal, partner, and public destinations.
expect(headersTo("https://carrier.example")).toEqual({
traceparent: expect.any(String),
baggage: "support.case_ref=case_8f3a"
});
expect(headersTo("https://public.example")).not.toHaveProperty("baggage");expect(headersTo("https://carrier.example")).toEqual({
traceparent: expect.any(String),
baggage: "support.case_ref=case_8f3a"
});
expect(headersTo("https://public.example")).not.toHaveProperty("baggage");5. Put a budget on baggage
The W3C specification requires compliant platforms to propagate up to 64 list members and 8,192 bytes, subject to its rules. Those are interoperability limits — not a design target and certainly not a confidentiality control. A smaller organizational budget makes accidental accumulation visible sooner and reduces header overhead.
A review checklist for the next outbound call
- What baggage keys are active at this call site?
- Who owns each key, and why is it propagated?
- Is the destination internal, an approved partner, public, or unknown?
- Does the egress path use an allowlist for that destination class?
- Could any value identify a person, tenant, experiment, release, route, or internal capability?
- Could a forged incoming value change behavior downstream?
- Do tests inspect the final carrier headers?
- Can the team disable a key without redeploying every service that receives it?
OpenTelemetry makes context propagation easier. That is the point. But ease of propagation changes the default failure mode: data can travel farther than the code that created it appears to suggest.
The practical answer is not to abandon baggage. It is to give baggage the same treatment as any other outbound data contract: classification, ownership, destination-specific policy, verification, and a safe default at the edge.