Skip to content

Troubleshooting Broken Traces and Context Propagation: OTCA practice questions

A trace stays connected only when every hop passes the current trace ID and span ID to the next one. Inside a process this travels in a Context object; across a network it is serialized into a carrier, usually HTTP headers, by a propagator.

By Ibrahim Yesilay · Published · Updated

Independent community practice, not affiliated with or endorsed by CNCF or The Linux Foundation.

What Troubleshooting Broken Traces and Context Propagation covers in OTCA

A trace stays connected only when every hop passes the current trace ID and span ID to the next one. Inside a process this travels in a Context object; across a network it is serialized into a carrier, usually HTTP headers, by a propagator. When a backend shows one request split into several unrelated traces, the cause is almost always a hop where that hand-off failed.

Start with configuration. The SDK environment variable OTEL_PROPAGATORS takes a comma-separated list, and its default is tracecontext,baggage, meaning W3C Trace Context plus W3C Baggage. Other known values are b3 for the B3 single-header format, b3multi for the B3 multi-header format, jaeger (deprecated), xray (third party) and ottrace (deprecated). Two services that speak different formats cannot read each other: a service that only extracts W3C headers sees nothing useful in B3 headers and starts a new trace. Listing several values builds a composite propagator that runs each one in order, which is the usual fix during a migration. A related B3 detail: the B3 propagator injects the single b3 header by default, so a legacy peer that only reads the X-B3 headers needs b3multi.

Remember also that the API itself uses no-op propagators until something configures them. Creating a TracerProvider and an exporter does not install a propagator on its own, so code-based setups must register one, while SDK auto-configuration reads OTEL_PROPAGATORS. Setting OTEL_SDK_DISABLED to true does not change propagators configured through OTEL_PROPAGATORS.

Extraction is designed to be forgiving. If a propagator cannot parse a value from the carrier, it must not throw and must not store a new value in the Context, so any earlier valid value is kept. A malformed incoming header therefore produces a span without a remote parent rather than an error, which hides the problem unless you inspect the raw headers.

In-process breaks are just as common. In Java the default ContextStorage keeps the current Context in a thread local, so work submitted to a plain executor runs on another thread with an empty Context and its spans become roots. The Context API offers wrappers for executors, runnables and suppliers that make the captured Context current before the task runs.

Infrastructure in the path matters too. The Propagators API expects instrumentation to extract and inject context on all remote calls, so any hop that rebuilds a request without injecting or forwarding the headers ends the trace there. Protocols without a metadata field need the context injected by hand and removed again on the receiving side before the payload is processed. On the other side, the context propagation docs suggest ignoring or sanitizing incoming context from untrusted external callers, and avoiding sensitive data in outgoing baggage.

Finally, baggage is often misread as a broken pipeline. Baggage entries are propagated next to the trace context, but they are a separate key-value store and do not become span, metric or log attributes until code, or a Baggage Span Processor in languages that ship one, copies them.

Key points

  • OTEL_PROPAGATORS defaults to tracecontext,baggage and accepts a comma-separated list that becomes a composite propagator.
  • Services using different formats, such as B3 and W3C, start separate traces unless both formats are configured.
  • The API uses no-op propagators until propagation is explicitly configured.
  • A propagator that cannot parse incoming headers must not throw and leaves the Context unchanged.
  • Baggage is propagated automatically but is not copied into span attributes unless code or a Baggage Span Processor does it.

Three sample questions

8 OTCA questions on Troubleshooting Broken Traces and Context Propagation, 8 with explanations. How practice works

Question 1

A public API accepts requests from third parties. Some callers send traceparent headers with the sampled flag set, which inflates tracing volume and mixes foreign trace IDs into your data. What do the OpenTelemetry and W3C guidance recommend?
  1. A.Remove "tracecontext" from OTEL_PROPAGATORS on all internal services
  2. B.Ignore or sanitize context from untrusted callers, e.g. restart the trace
  3. C.Rely on the sampled flag, because W3C requires every receiver to honour it
  4. D.Move the trace ID into baggage so external callers cannot change it at all

Correct answer: B

The OpenTelemetry context propagation docs advise caution with incoming context from external sources: malicious actors can send forged trace headers, and you might want to ignore or sanitize incoming context from untrusted sources. W3C Trace Context warns that a service with a public API that naively continues any trace with the sampled flag set can be overwhelmed with tracing overhead or forged trace-id collisions, and it describes "restart trace" (regenerating trace-id, parent-id and flags) for services that act as a front gate. Why the other options are wrong. "Remove "tracecontext" from OTEL_PROPAGATORS on all…": that would break propagation between your own services too; the guidance is about the boundary with untrusted callers. "Rely on the sampled flag, because W3C requires eve…": the W3C spec calls trace-flags recommendations from the caller, not strict rules, and warns against naively continuing sampled traces from public callers. "Move the trace ID into baggage so external callers…": baggage is also sent in plain request headers and has no built-in integrity checks, so it is no safer than traceparent.

Sources: OpenTelemetry docs: Context propagation, W3C Trace Context

Question 2

During a migration from Zipkin, a service must keep extracting and injecting W3C Trace Context and W3C Baggage while also exchanging B3 multi-header (X-B3-*) context with legacy services. Which OTEL_PROPAGATORS value configures this?
  1. A.w3c,baggage,zipkin
  2. B.traceparent,tracestate,b3
  3. C.tracecontext;baggage;b3-multi
  4. D.tracecontext,baggage,b3multi

Correct answer: D

OTEL_PROPAGATORS is a comma-separated list of known values. "tracecontext" is W3C Trace Context, "baggage" is W3C Baggage and "b3multi" is B3 Multi (multi-header). The listed propagators are combined into a composite propagator, so all three formats are handled; values must be deduplicated so each propagator is registered once. Why the other options are wrong. "w3c,baggage,zipkin": "w3c" and "zipkin" are not known OTEL_PROPAGATORS values; the W3C trace format is named "tracecontext" and the Zipkin formats are "b3" and "b3multi". "traceparent,tracestate,b3": "traceparent" and "tracestate" are header names, not propagator names, and "b3" selects the B3 single-header format rather than the multi-header one the legacy services need. "tracecontext;baggage;b3-multi": the variable is a comma-separated list, not semicolon-separated, and the known value is spelled "b3multi" without a hyphen.

Sources: OpenTelemetry spec: SDK environment variables, OpenTelemetry spec: Propagators API

Question 3

A Java service handles a request inside an active server span and submits work to a plain ExecutorService created with Executors.newFixedThreadPool(4). Spans created inside the submitted tasks show up as separate root spans instead of children of the request span. What explains this, and what fixes it?
  1. A.Context is stored thread-locally by default; wrap the executor or task with Context.wrap
  2. B.No traceparent header is sent between threads, so each task needs a TextMapPropagator inject call first
  3. C.The tracer sampled the child spans out, which makes the backend show them as root spans
  4. D.OTEL_PROPAGATORS is unset, so in-process context cannot be shared across thread pools

Correct answer: A

The OpenTelemetry Java API docs state that the default ContextStorage implementation stores Context in a thread local. A task running on a different pool thread therefore sees an empty current Context and starts a new root span. The Context API provides utilities such as context.wrap(executorService), context.wrap(runnable) and wrapSupplier that call makeCurrent() before running the work, so the parent span travels with the task. The Context spec describes this as associating a Context with an execution unit (attach/detach). Why the other options are wrong. "No traceparent header is sent between threads, so…": inject/extract is for cross-process carriers such as HTTP headers; within one process the Context object is passed or made current, there are no headers between threads. "The tracer sampled the child spans out, which make…": sampling decides whether spans are recorded, it does not change their parent; an unsampled child would be missing, not re-parented as a root. "OTEL_PROPAGATORS is unset, so in-process context c…": OTEL_PROPAGATORS selects cross-process propagation formats and defaults to "tracecontext,baggage"; it has no effect on handing Context between threads.

Sources: OpenTelemetry docs: Java API (Context), OpenTelemetry spec: Context

Practice this topic

Focus mode keeps every question on Troubleshooting Broken Traces and Context Propagation until you leave it. 10 free guest answers, then a free account; no credit card.

Or open OTCA practice across all topics.

Sources

The explanation above was checked against these official documents.

Frequently asked questions

Why do spans created in a thread pool show up as root spans?

The current Context is stored per thread by default, so a task on a pool thread does not see the caller's span. Wrapping the executor or the task with the Context utilities makes the captured Context current when the task runs.

What is the difference between b3 and b3multi?

Both extract either B3 encoding, but b3 injects the single b3 header while b3multi injects the separate X-B3 headers.