tracer.start_as_current_span(...), OTel reads the current context, sets the new span as a child, and updates the context.
Within a single thread of execution, this is automatic. Across threads, async boundaries, or service boundaries, the context doesn’t follow on its own — you have to propagate it.
The Context Object
Every span carries an immutable Context object containing:
Baggage is the channel for non-tracing data that should follow the request — feature flags, tenant IDs, request type. Baggage doesn’t become span attributes automatically; if you want it on spans, you have to read it and set it explicitly.
When Propagation Is Needed (and When It Isn’t)
Most application code falls into the left column — context propagation just works. The right column is where you need the propagators below.
Automatic Propagation
In Python, OTel usescontextvars under the hood, which propagates context cleanly across:
- Synchronous function calls.
asynciotasks within the same event loop.awaitboundaries.
tracer.start_as_current_span(...) works as you’d expect.
Manual Context Propagation
When automatic propagation breaks down — across threads, across services, into background workers — these are the tools OTel gives you:
The OTel Python API exposes
opentelemetry.propagate.inject and opentelemetry.propagate.extract, which delegate to the global text-map propagator. By default that propagator is a CompositePropagator combining:
You can swap the global propagator with
propagate.set_global_textmap(...) if you need a different combination — for example, adding B3 for legacy systems. The OTel specification just requires SDKs to default to a composite of the W3C Trace Context and Baggage propagators; JS/Go/Java equivalents construct the same composite under slightly different names. Check the Propagators spec for your language.
For the Python API reference, see OpenTelemetry Context API and Propagators API.
Async Functions (Same Service)
When you launch async work from sync code, the context doesn’t always follow. Capture it explicitly:- Capture the current context before launching async work:
context = get_current(). - Pass the context into the async function.
- Inside the async function,
attach(ctx)and store the returned token. - In a
finally,detach(token)to restore the previous context.
Across Microservices
Crossing a service boundary is where propagators earn their keep. Service A injects the current context into outbound request headers; Service B extracts it on the inbound side and uses it as the parent for its own spans. Service A (caller):Common Propagation Failure Modes
A few patterns that produce orphaned or broken traces:- Forgetting to inject context on the caller side — the callee starts a new trace instead of joining the existing one. Symptom: caller and callee appear as separate traces in the Phoenix UI.
- Using different propagators on each side — if Service A injects W3C headers and Service B only extracts a different format, the context is silently lost. Stick with the default global propagator on both sides unless you have a specific reason not to.
- Stripping headers in the network layer — proxies, API gateways, and service meshes sometimes strip headers they don’t recognize. The W3C
traceparentheader is usually safe but verify with your infrastructure team. - Async work that escapes the request lifecycle — fire-and-forget tasks (background workers, queues) need explicit context capture before submission.

