A trace holds together because every span knows its parent. Context propagation moves that parent reference across two boundaries: from function to function inside one process, and from service to service over the network. Break either one and SigNoz shows two short traces where you expected a single request.
Overview
OpenTelemetry keeps the active span in a Context object. The specification describes context as an object that contains the information for the sending and receiving service, or execution unit, to correlate one signal with another. When you start a span, the tracer reads the current context. It finds the span already sitting there and records that span's ID as the new span's parent.
None of this reaches SigNoz as configuration. Propagation happens inside your application before a span is ever exported, so a trace arrives complete or arrives broken.
Instrumentation libraries handle both boundaries for the frameworks and clients they cover. You do the work yourself at any hop they miss.
Inside a process
How a child span finds its parent depends on what your language does with ambient state.
| Language | Where the active span lives | What you have to do |
|---|---|---|
| Go | An argument you pass by hand | Thread ctx through every call, including goroutines |
| Rust | A thread-local Context, or an argument | Attach the context, and wrap a spawned future with with_context |
| Java | Thread-local storage | Activate the span, close the scope, and wrap work you hand to a thread pool |
| .NET | An AsyncLocal, so it follows await and Task.Run | Activate the span, and pass an ActivityContext to a thread that predates it |
| Ruby, PHP | Context storage the runtime manages | Activate the span and detach the scope when the block ends |
| Python, Node.js, Deno | contextvars and AsyncLocalStorage | Use the active-span helper instead of starting a detached span |
Go is the only one with no ambient state at all: every function that starts a span needs the context its caller was holding. Rust keeps a thread-local current context, so attach works inside one thread, but a task moved to another thread needs the context carried across. The rest keep the active span in storage the runtime manages. That covers straight-line code, and where it stops differs: Java drops it at a thread pool, while .NET carries it through await and Task.Run.
Across a process boundary
Two operations move context over the network:
- Inject writes the current context into a carrier the transport can carry, usually a map of HTTP headers.
- Extract reads that carrier on the receiving side and returns a context. You make it current, then start the entry span inside it.
Set the span kind at the same time. Mark a request and response hop Client on the caller and Server on the receiver. Mark a message queue hop Producer on the publisher and Consumer on the reader. SigNoz uses these to build the service map and APM metrics.
A propagator owns both operations and decides the wire format. Both services have to agree on that format. A service writing B3 headers to a service reading W3C Trace Context produces an empty extraction and a fresh trace on every hop.
What the propagator puts on the wire
The default propagator writes a traceparent header defined by the W3C Trace Context specification:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01| Field | Example | What it carries |
|---|---|---|
| version | 00 | The Trace Context version in use |
| trace-id | 4bf92f3577b34da6a3ce929d0e0e4736 | 16 bytes, identical on every span in the trace |
| parent-id | 00f067aa0ba902b7 | 8 bytes, the caller's span ID, which becomes the parent of the receiver's entry span |
| trace-flags | 01 | 1 byte; the rightmost bit records whether the caller sampled the trace |
Two more headers travel alongside it. tracestate holds vendor-specific key-value pairs and stays optional. baggage carries arbitrary key-value pairs you set yourself, such as a tenant ID. Keep secrets and personal data out of baggage, since downstream services and their logs receive whatever you put there.
When you propagate manually
When no library covers the hop, write the inject and extract calls yourself:
- A message queue or event bus with no instrumentation library. Put the trace context on the message when you publish it. Read it back in the consumer.
- A custom or binary protocol that carries no HTTP headers.
- A background worker, scheduled job, or thread pool that starts work after the request that scheduled it has returned.
- An HTTP client your instrumentation does not wrap.
View a connected trace in SigNoz
Open the Trace Explorer and select a request that crosses a service boundary. The waterfall names the service on every row, so a connected trace shows the service change while the indentation continues.

handle-charge runs in payments, and it sits under charge-card from checkout. Both spans carry one trace ID, which is what propagation produced.
Select a span to open Span details. The parent_span_id field holds the caller's span ID. The is_remote field reports yes for a parent that arrived from another process.

Spot a broken trace in SigNoz
Open the Trace Explorer. A propagation failure leaves a recognizable shape:
- The request you were following stops at the service boundary, and the downstream work sits in a separate trace under its own trace ID.
- The downstream trace's root span is your own HTTP handler rather than the caller's span.
- The upstream span covers the network call, but nothing inside it accounts for the time.
Open the downstream trace and look at its root span. When a span that needs a parent is a root, the caller never injected or the receiver never extracted.
Propagate context in your language
Each manual instrumentation guide covers the in-process rules for that language and shows a working inject and extract pair.
| Language | Guide | Propagator entry point |
|---|---|---|
| Go | Manual instrumentation in Go | otel.GetTextMapPropagator() |
| Python | Manual instrumentation in Python | opentelemetry.propagate.inject and extract |
| Java | Manual instrumentation in Java | ContextPropagators.getTextMapPropagator() |
| Node.js | Manual instrumentation in Node.js | propagation.inject and propagation.extract |
| .NET | Manual instrumentation in .NET | Propagators.DefaultTextMapPropagator |
| PHP | Manual instrumentation in PHP | Globals::propagator() |
| Rust | Manual instrumentation in Rust | global::get_text_map_propagator() |
| Ruby | Manual instrumentation in Ruby | OpenTelemetry.propagation |
| Deno | Manual instrumentation in Deno | propagation.inject and propagation.extract |
Common mistakes
Each of these breaks a trace without raising an error.
| Mistake | What happens | Fix |
|---|---|---|
| The two services register different propagators | One side writes b3, the other reads traceparent. Every extraction returns an empty context. | Register the same format on both sides, or register a composite propagator that accepts both. |
| You inject before the SDK configures a propagator | In .NET, Propagators.DefaultTextMapPropagator stays a no-op until the SDK sets it. An early inject writes nothing. | Build the tracer provider first, then inject. |
| You start the entry span from a fresh root context | The entry span becomes a root and opens a new trace. | Pass the context that extract returned, not context.Background(), Context.root(), or an empty carrier. |
| Work crosses a boundary the runtime does not carry context across | A Java thread pool, a bare Python or Ruby thread, a Go goroutine, and a Rust task moved to another thread all start with no context, so the new span becomes a root. .NET is the exception: Activity.Current is an AsyncLocal, so it follows await and Task.Run. | Wrap the executor, capture the context before you spawn, or pass it in as an argument. |
| A scope stays open | Java and PHP hand you a scope to close. Later spans attach to the wrong parent. | Call detach(), or use a try-with-resources block. |
| The carrier's header names are the wrong case | Python and Ruby look up the literal lowercase traceparent. A carrier holding Traceparent extracts nothing. | Lowercase the keys when you build the carrier. PHP is case-insensitive, and Node lowercases incoming headers for you. |
Next steps
- Span Links for work that crosses an async boundary and belongs in its own trace
- Correlate Traces and Logs to carry the trace ID into your log lines
- Trace Details for reading a trace once the spans connect
Get Help
If you need help with the steps in this topic, please reach out to us on SigNoz Community Slack. If you are a SigNoz Cloud user, please use in product chat support located at the bottom right corner of your SigNoz instance or contact us at cloud-support@signoz.io.