Trace Propagation
How SDKs propagate trace context between services via headers, metadata, and environment variables.
SDKs MUST propagate trace information between services so that all telemetry (errors, profiles, replays, transactions, check-ins, etc.) from those services can be connected into one trace.
Trace propagation operates independently of span collection. Even when an SDK is not configured to send spans (i.e. no tracesSampleRate or tracesSampler), it MUST still continue incoming traces, propagate trace data on outgoing requests, and attach trace context to events. This default behavior is sometimes referred to as "Tracing without Performance" (TwP).
Related specs:
- Dynamic Sampling Context — DSC fields, baggage format, freezing rules
- Traces — span collection, sampling, transaction payloads
- Scopes — propagation context storage
Trace context: The combination of
trace_id,span_id, and sampling decision that identifies a position within a distributed trace.Propagation context: The storage location for trace context on the current scope. The propagation context SHOULD be populated with a random
traceIdandspanIdif no incoming trace is present.Carriers: The mechanisms used to transmit trace context between services:
- HTTP headers:
sentry-traceandbaggageheaders on outgoing HTTP requests. - Metadata:
sentry-traceandbaggageas metadata on queue messages (details are queue-specific). - Environment variables:
SENTRY_TRACEandSENTRY_BAGGAGEwhen calling another process.
- HTTP headers:
SDKs MUST propagate trace context to downstream services by adding sentry-trace and baggage to outgoing requests via one of the supported carriers.
The sentry-trace and baggage headers MUST only be attached to an outgoing request if the request URL matches the tracePropagationTargets option (or if the option is null / not set).
SDKs MUST pick up incoming trace information by:
- Reading
sentry-traceandbaggageheaders for each incoming HTTP request. - Reading
sentry-traceandbaggagemetadata when retrieving an item from a queue. - Reading the environment variables
SENTRY_TRACEandSENTRY_BAGGAGEon startup.
This trace information MUST be stored in the propagation context of the current scope, ensuring all telemetry emitted from the receiving service includes the correct trace information.
SDKs MUST expose a tracePropagationTargets option that controls which outgoing HTTP requests receive trace headers. This option takes an array of strings and/or regular expressions.
SDKs MUST only add trace headers to an outgoing request if the request's URL matches the regex or, in the case of string literals, contains at least one of the items from the array. String literals do not have to be full matches — the URL of a request is matched when it contains a string provided through the option.
SDKs MAY choose a default value which makes sense for their use case. Most SDKs default to .* (attach headers to all outgoing requests), but deviation is allowed. For example, because of CORS, browser-based SDKs default to only adding headers to domain-internal requests.
// Entries can be strings or regex
tracePropagationTargets: ['localhost', /^\// ,/myApi.com\/v[2-4]/]
URLs matching: 'localhost:8443/api/users', 'mylocalhost:8080/api/users', '/api/envelopes', 'myApi.com/v2/projects'
URLs not matching: 'someHost.com/data', 'myApi.com/v1/projects'
Deprecation of tracingOrigins
This option replaces the non-standardized tracingOrigins option which was previously used in some SDKs. SDKs that support tracingOrigins are encouraged to deprecate and eventually remove tracingOrigins in favour of tracePropagationTargets. In case both options are specified by users, SDKs SHOULD only rely on the tracePropagationTargets array.
The strictTraceContinuation option controls whether to continue a trace when either the incoming trace or the receiving SDK has an organization ID, but not both. This scenario typically would happen if the incoming trace originates from a third-party service instrumented with Sentry.
SDKs MUST support the following configuration options:
strictTraceContinuation: MUST be a boolean value. Default isfalse.orgId: optional override of the organization ID parsed from the DSN.- SDK documentation for the
orgIdoption SHOULD recommend settingorgIdfor self-hosted Sentry and local Relay setups.
- SDK documentation for the
Once most SDKs support the strictTraceContinuation option, we intend to migrate the default value to true. As this change is breaking, it will be part of a major release.
The SDK MUST determine its organization ID as follows:
- If
orgIdis present, the SDK's organization ID is the value passed toorgId. - Otherwise, the SDK's organization ID is the organization ID in the DSN.
- If
orgIdis absent and the DSN cannot be parsed, then the organization ID is missing.
If the SDK has an organization ID, the SDK MUST propagate the organization ID in baggage as sentry-org_id.
On incoming traces, the SDK MUST compare the incoming sentry-org_id with its own organization ID, as defined above.
- If both values are present and differ, the SDK MUST NOT continue the trace, regardless of
strictTraceContinuation. - If exactly one value is present, the SDK MUST NOT continue when
strictTraceContinuationistrue, and SHOULD continue when it isfalse. - If both values are present and equal, or both are absent, the SDK SHOULD continue the trace.
When the SDK does not continue a trace, the incoming trace ID, parent sampling decision, and incoming baggage SHOULD be ignored. In this case, the SDK SHOULD behave as the head of a trace.
Baggage sentry-org_id | SDK organization ID | strictTraceContinuation | Result |
|---|---|---|---|
| 1 | 1 | false | continue trace |
| none | 1 | false | continue trace |
| 1 | none | false | continue trace |
| none | none | false | continue trace |
| 1 | 2 | false | start new trace |
| 1 | 1 | true | continue trace |
| none | 1 | true | start new trace |
| 1 | none | true | start new trace |
| none | none | true | continue trace |
| 1 | 2 | true | start new trace |
This MUST be a boolean value. The default is false. This option enables the propagation of the W3C Trace Context HTTP header traceparent on outgoing HTTP requests.
The traceparent header MUST only be sent when propagateTraceparent is true and the request matches tracePropagationTargets (same condition as for sentry-trace/baggage).
Header Format:
- Key:
traceparent - Value:
00-<traceId>-<spanId>-<sampled>where<sampled>is01if sampled, otherwise00.
The full spec is available in the W3C Trace Context specification.
To improve the likelihood of capturing complete traces when backend services use a custom sample rate via tracesSampler, the SDK propagates the same random value used for sampling decisions across all services in a trace. This ensures consistent sampling decisions across a trace instead of generating a new random value for each service.
The random value (sample_rand) is set according to the following rules:
- A
sample_randis a float (0.123456notation) in the range of[0, 1)(including 0.0, excluding 1.0), with six digits after the decimal point. - When tracing is enabled and a new trace is started, the SDK generates a
sample_randvalue for the current execution context.- This also applies to default propagation mode (no tracing config) where the SDK simply generates
sample_randevery time a new trace is started. Thissample_randvalue can then be stored onPropagationContext.
- This also applies to default propagation mode (no tracing config) where the SDK simply generates
- It is recommended to generate the random number deterministically using the trace ID as seed or source of randomness (make sure to do this atomically so multiple threads accessing the same random instance don't mix their seeds). The exact method by which the random number is created is implementation-defined and MAY vary between SDK implementations.
- The
sample_randis part of the DSC (Dynamic Sampling Context) and as with other values on thebaggageheader, thesample_randvalue from the current execution context SHOULD be propagated to downstream SDKs. It SHOULD also be sent to other systems as part of thebaggageheader if Performance is disabled and sampling decision is deferred. - On incoming traces, an SDK takes the incoming
sentry-sample_randvalue in thebaggageheader and uses it for the rest of the current execution context (for example, request) by storing it in thePropagationContext. - If
sample_randis missing on an incoming trace, the SDK creates a new one applying special rules and uses it for the current execution context:- If
sample_rate(insidebaggage) and the sampling decision (trailing-1or-0from thesentry-traceheader) are present in the incoming trace, createsample_randso that when compared to the incomingsample_rateit would lead to the same sampling decision that is in thesentry-traceheader. This means, for a decision ofTruegenerate a random number in half-open range[0, rate)and for a decision ofFalsegenerate a random number in range[rate, 1). - If the sampling decision is missing on the incoming trace, generate a random number in range of
[0, 1)(including 0.0, excluding 1.0), like for a new trace.
- If
The SDK SHOULD always use the stored random number (sentry-sample_rand) for sampling decisions and SHOULD NOT directly rely on math.random() when deciding to sample or not:
- When the
tracesSampleris invoked, the return value SHOULD be compared withsample_rand:trace["sentry-sample_rand"] < tracesSampler(context) - When there is an incoming trace, the SDK fully inherits incoming sampling decisions from the
sentry-traceheader. - Otherwise, when the SDK is the head of a trace (and
tracesSampleRateis defined), the sampling is done comparing thetracesSampleRatewith thesample_rand:trace["sentry-sample_rand"] < config.tracesSampleRate
Something that SHOULD be documented for every SDK is the recommended way to use a tracesSampler to inherit the parent's sampling decision using the parentSampleRate. This way, Sentry can still extrapolate counts correctly:
tracesSampler: ({ name, parentSampleRate }) => {
// Inherit the trace parent's sample rate if there is one. Sampling is deterministic
// for one trace, i.e. if the parent was sampled, we will be sampled too at the same
// rate.
if (typeof parentSampleRate === "number") {
return parentSampleRate;
}
// Else, use default sample rate (replacing tracesSampleRate).
return 0.5;
},
A transaction's sampling decision MUST be passed to all of its children, including across service boundaries. This can be accomplished in the startChild method for same-service children and using the sentry-trace header for children in a different service.
If Sentry SDKs are not configured for sending spans (i.e. no tracesSampleRate or tracesSampler), they MUST fall back to a mode where they still handle continuing and propagating traces. Internally, this mode is referred to as "Tracing without Performance" (TwP).
This means that SDKs by default always:
- Continue incoming traces
- Attach
event.contexts.tracecontext on events (e.g. errors, check-ins, replays) - Attach the
traceenvelope header to Sentry envelopes, populated from the dynamic sampling context - Propagate trace data (
sentry-trace,baggage) via the usual carriers, with the correct sampling decision
Setting tracesSampleRate: 0 does not fall back to this mode. Instead, sampling decisions are always negative and the trace is propagated with a negative sampling decision.
To opt out of further propagating a trace, users can pass an empty array (or language-equivalent parameter) to tracePropagationTargets. This prevents the SDK from propagating trace information further. Note that for incoming requests with trace headers, SDKs SHOULD still continue the trace but not propagate it further downstream.
If an SDK in default propagation mode doesn't receive an incoming trace, it SHOULD start a new trace. In this case, the new trace is not sampled (as in, there is no sampling decision, neither positive nor negative). Instead, the sampling decision is deferred to the next downstream SDK.
This means that:
- The SDK MUST NOT include a sampled flag in the
sentry-traceheader, meaning the header has the format<traceId>-<spanId>. - The dynamic sampling context, propagated via
baggage, MUST NOT contain thesentry-sampledkey.
For SDKs starting a new trace in this mode, the Dynamic Sampling Context SHOULD be lazily populated and frozen for the duration of the trace. Given that no span is actually available, the DSC will not contain any keys related to spans (transaction, sample_rate, or sampled).
Any event created by an SDK MUST include the trace context. This context SHOULD contain the trace data of the current trace, if available, just like in regular tracing mode.
Furthermore, the trace envelope header (populated from the dynamic sampling context) MUST be attached to any outgoing event envelope.
Traces SHOULD have the same duration as regular traces. For example, a trace for a backend server SHOULD generally last for the duration of one individual request. This usually corresponds with the lifetime of an isolation scope.
SDKs MUST store trace data in a way that makes it possible for them to be attached to events and propagated to outgoing requests. The exact storage mechanism is up to the SDK implementation, but we recommend storing the data on the scope in a field called propagationContext.
The propagationContext SHOULD be populated with a random traceId and spanId if no incoming trace is present. This — in combination with the sentry-trace header specification requiring a spanId — means a non-existing spanId will be propagated along with the trace and attached to events. While not ideal, we accept this limitation as the Sentry product can and SHOULD handle non-existing (parent) spans.
In SDKs adapting OpenTelemetry's tracing capabilities (POTel), trace data could also be stored in a non-recording span. Note that in the case of using the non-recording span, the span is also not sampled, meaning the sampling decision MUST still be deferred when starting a new trace.
The sentry-trace header is used for trace propagation. SDKs use it to continue traces from upstream services (incoming HTTP requests) and to propagate tracing information to downstream services (outgoing HTTP requests).
sentry-trace = traceid-spanid-sampled
sampled is optional. So at a minimum, it's expected:
sentry-trace = traceid-spanid
To offer minimal compatibility with the W3C traceparent header (without the version prefix) and Zipkin's b3 headers (which consider both 64 and 128 bits for traceId valid), the sentry-trace header SHOULD have a traceId of 128 bits encoded in 32 hex chars and a spanId of 64 bits encoded in 16 hex chars. To avoid confusion with the W3C traceparent header (to which our header is similar but not identical), we call it simply sentry-trace. No version is being defined in the header.
The sentry-trace header MUST only be attached to an outgoing request if the request's URL matches at least one entry of the tracePropagationTargets option or this option is set to null.
To simplify processing, the value consists of a single (optional) character. The possible values are:
- No value means defer
0 - Don't sample
1 - Sampled
Unlike with b3 headers, a sentry-trace header MUST never consist solely of a sampling decision, with no traceid or spanid values. There are good reasons to always include the traceid and spanid regardless of the sampling decision, and doing so also simplifies implementation.
Besides the usual reasons to use *defer,* in the case of Sentry, a reason would be if a downstream system captures an error event with Sentry. The decision could be done at that point to sample that trace in order to have tracing data available for the reported crash.
To interoperate with OpenTelemetry (OTel) services, SDKs can optionally send the W3C Trace Context traceparent HTTP header on outgoing requests in addition to Sentry's own sentry-trace and baggage. This enables traces originating from a Sentry SDK to be continued by components that only use OTel/W3C propagation.
The traceparent header MUST only be sent when propagateTraceparent is true and the request matches tracePropagationTargets (same condition as for sentry-trace/baggage).
The full spec is available in the W3C Trace Context document.
When propagating trace context by launching a subprocess, the environment variables SENTRY_TRACE and SENTRY_BAGGAGE SHOULD be set. The values MUST match what would be set for the corresponding HTTP headers.
To continue a trace from an upstream service, the SDK MUST expose a method to extract the traceparent and baggage information and apply these to the applicable scope. The method MUST NOT create a new segment span on its own.
Sentry.continueTrace({
sentryTrace: request.headers['sentry-trace'],
baggage: request.headers['baggage'],
}, () => {
Sentry.startSpan({ name: 'test' }, () => {
// ....
});
})
Newly created root spans SHOULD now contain these properties, such as trace_id and parent_span_id.
The exact function signature of the continueTrace function depends on what's canonical in the SDK. It MAY require explicitly passing sentry-trace and baggage, or it MAY allow providing a dictionary of headers/environment variables.
To propagate a trace to a downstream service, the SDK MUST expose methods to fetch the required information to allow the next service to continue the trace.
const traceData = Sentry.getTraceData()
The SDK MUST offer a method to clear trace propagation data, allowing to create spans with a fresh new trace.
Sentry.startNewTrace(() => {
Sentry.startSpan({ name: 'segment under trace 1' }, () => {...});
})
Sentry.startNewTrace(() => {
Sentry.startSpan({ name: 'segment under trace 2' }, () => {...});
})
This matrix shows how trace propagation works in SDKs under different scenarios.
Column definitions:
Scenario (inputs):
- Incoming trace: Whether
sentry-trace/baggageheaders are present on the incoming request. - Incoming sampled: The sampled flag from the incoming
sentry-traceheader (1= sampled,0= not sampled,deferred= no value). tracePropagationTargetsmatch: Whether the outgoing request URL matches thetracePropagationTargetsoption.tracesSampleRate: The configured sample rate (null= tracing not enabled,0= 0%,1= 100%).
Outcome (results):
- Send spans?: Whether the SDK sends span/transaction data to Sentry.
- Outgoing trace?: Whether
sentry-trace/baggageheaders are added to outgoing requests. - Continue trace?: Whether the outgoing
trace_idmatches the incomingtrace_id.
| Incoming trace | Incoming sampled | tracePropagationTargets match | tracesSampleRate | Send spans? | Outgoing trace? | Continue trace? |
|---|---|---|---|---|---|---|
| not present | - | yes | null | no | yes | - |
| not present | - | yes | 0 | no | yes | - |
| not present | - | yes | 1 | yes | yes | - |
| not present | - | no | null | no | no | - |
| not present | - | no | 0 | no | no | - |
| not present | - | no | 1 | yes | no | - |
| present | deferred | yes | null | no | yes | yes |
| present | deferred | yes | 0 | no | yes | yes |
| present | deferred | yes | 1 | yes | yes | yes |
| present | 1 | yes | null | no | yes | yes |
| present | 1 | yes | 0 | yes | yes | yes |
| present | 1 | yes | 1 | yes | yes | yes |
| present | 0 | yes | null | no | yes | yes |
| present | 0 | yes | 0 | no | yes | yes |
| present | 0 | yes | 1 | no | yes | yes |
| present | deferred | no | null | no | no | - |
| present | deferred | no | 0 | no | no | - |
| present | deferred | no | 1 | yes | no | - |
| present | 1 | no | null | no | no | - |
| present | 1 | no | 0 | yes | no | - |
| present | 1 | no | 1 | yes | no | - |
| present | 0 | no | null | no | no | - |
| present | 0 | no | 0 | no | no | - |
| present | 0 | no | 1 | no | no | - |
Why are we sometimes sampling even if tracesSampleRate is 0? Why are we sometimes unsampling if tracesSampleRate is 1?
A tracesSampleRate between 0 and 1 is only taken into account if there is no incoming trace and the SDK has to make its own sampling decision. If there is an incoming trace, the parent sampling decision always takes precedence over tracesSampleRate.
| Version | Date | Summary |
|---|---|---|
1.9.0 | 2026-02-24 | Consolidated into foundations/trace-propagation |
1.8.0 | 2026-01-22 | Added startNewTrace API |
1.7.0 | 2025-09-09 | Added propagateTraceparent option |
1.6.0 | 2025-08-14 | Re-added W3C traceparent documentation |
1.5.0 | 2024-11-06 | Added default propagation behavior without tracing (TwP), deferred sampling |
1.4.0 | 2025-04-09 | Added propagation decision matrix |
1.3.0 | 2024-11-28 | Added strictTraceContinuation, sample_rand propagation |
1.2.0 | 2024-04-09 | Added W3C traceparent header support |
1.1.0 | 2023-11-06 | Distributed tracing (carriers, mechanisms, env vars) |
1.0.0 | 2022-07-21 | Initial — tracePropagationTargets, sentry-trace header |
Our documentation is open source and available on GitHub. Your contributions are welcome, whether fixing a typo (drat!) or suggesting an update ("yeah, this would be better").