Streamed Spans
Learn how to use stream mode to send spans to Sentry as they finish, removing the 1,000-span limit and making trace data visible sooner.
By default, the Sentry Python SDK collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. This is called transaction mode. Stream mode changes this by sending spans to Sentry in batches as they finish. Service spans, which represent a service's entry point, replace transactions as the main grouping for each service.
You can find the following span types mentioned throughout this page:
- Root span: The topmost span in a trace. It has no parent span and is always a service span.
- Service span: A parent-level span at the entry of a service. In transaction mode, this is called a transaction.
- Child span: Any span nested under a parent span within the same trace.
This graph shows how these span types relate to each other within a trace:
Trace
│
└── Root span [service A]
├── Child span
│ └── Child span
└── Service span [service B]
├── Child span
└── Child span
Trace
│
└── Root span [service A]
├── Child span
│ └── Child span
└── Service span [service B]
├── Child span
└── Child span
Migrating from transaction mode?
Stream mode requires the streamed Span API, so migrating to stream mode and migrating to the streamed Span API are the same step. If you have existing custom instrumentation, see the Migration Guide for a full list of changes.
You need:
- Tracing configured in your app
- Sentry SDK
>=2.62.0
Opt in by adding the trace_lifecycle option when initializing the SDK:
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
# enables stream mode
trace_lifecycle="stream",
)
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
# enables stream mode
trace_lifecycle="stream",
)
To revert to transaction mode, remove the trace_lifecycle option or set it to "static" (the default).
Use sentry_sdk.traces.start_span() to create a span that ends automatically when the with block exits:
import sentry_sdk
with sentry_sdk.traces.start_span(name="my-operation") as span:
# Your code here
do_work()
import sentry_sdk
with sentry_sdk.traces.start_span(name="my-operation") as span:
# Your code here
do_work()
Child spans created inside an active span are automatically associated with the parent:
import sentry_sdk
with sentry_sdk.traces.start_span(name="parent-operation"):
with sentry_sdk.traces.start_span(name="child-step-1"):
step_one()
with sentry_sdk.traces.start_span(name="child-step-2"):
step_two()
import sentry_sdk
with sentry_sdk.traces.start_span(name="parent-operation"):
with sentry_sdk.traces.start_span(name="child-step-1"):
step_one()
with sentry_sdk.traces.start_span(name="child-step-2"):
step_two()
A span is automatically promoted to a service span (the equivalent of a transaction) if no other parent is currently active. If you want to force a new service span, regardless of whether it has a parent span, set parent_span=None:
import sentry_sdk
with sentry_sdk.traces.start_span(name="task-name", parent_span=None) as span:
do_work()
import sentry_sdk
with sentry_sdk.traces.start_span(name="task-name", parent_span=None) as span:
do_work()
You can also use the @trace decorator to instrument a function. It accepts optional name, attributes, and active arguments:
from sentry_sdk.traces import trace
@trace(name="checkout", attributes={"flow.pipeline": "legacy"})
def checkout():
...
from sentry_sdk.traces import trace
@trace(name="checkout", attributes={"flow.pipeline": "legacy"})
def checkout():
...
For more details on span creation, see Custom Instrumentation.
Attach structured metadata to spans using attributes, which can be str, int, float, or bool, as well as arrays of these types.
Sentry automatically sets several standard attributes on spans. To avoid accidentally overwriting these, refer to our Sentry Attribute Conventions.
You can set attributes when starting a span:
import sentry_sdk
with sentry_sdk.traces.start_span(
name="process-order",
attributes={
"sentry.op": "queue.process",
"order.id": "abc-123",
"order.item_count": 5,
"order.priority": True,
},
):
process_order()
import sentry_sdk
with sentry_sdk.traces.start_span(
name="process-order",
attributes={
"sentry.op": "queue.process",
"order.id": "abc-123",
"order.item_count": 5,
"order.priority": True,
},
):
process_order()
Or add them to an already running span:
import sentry_sdk
with sentry_sdk.traces.start_span(name="handle-request") as span:
# Set a single attribute
span.set_attribute("http.response.status_code", 200)
# Set multiple attributes at once
span.set_attributes({
"http.route": "/api/users",
"user.id": "user-42",
})
import sentry_sdk
with sentry_sdk.traces.start_span(name="handle-request") as span:
# Set a single attribute
span.set_attribute("http.response.status_code", 200)
# Set multiple attributes at once
span.set_attributes({
"http.route": "/api/users",
"user.id": "user-42",
})
Find more examples in our Sending Span Metrics documentation.
When your service receives a request from an upstream service that includes Sentry trace headers, use sentry_sdk.traces.continue_trace() to connect your spans to the existing distributed trace. Unlike the legacy sentry_sdk.continue_trace(), the new version is not a context manager. Instead, it sets the propagation context and the next span picks it up automatically.
import sentry_sdk
headers = {
"sentry-trace": "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0be902b7-1",
"baggage": "sentry-trace_id=...",
}
sentry_sdk.traces.continue_trace(headers)
with sentry_sdk.traces.start_span(name="handle request"):
...
import sentry_sdk
headers = {
"sentry-trace": "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0be902b7-1",
"baggage": "sentry-trace_id=...",
}
sentry_sdk.traces.continue_trace(headers)
with sentry_sdk.traces.start_span(name="handle request"):
...
If you need to start a completely new trace unconnected to the current one, use sentry_sdk.traces.new_trace(). This is useful for background jobs or scheduled tasks where you want a clean trace boundary:
import sentry_sdk
with sentry_sdk.traces.start_span(name="span in trace 1"):
...
sentry_sdk.traces.new_trace()
with sentry_sdk.traces.start_span(name="span in trace 2"):
# This span is the root of a new, separate trace
...
import sentry_sdk
with sentry_sdk.traces.start_span(name="span in trace 1"):
...
sentry_sdk.traces.new_trace()
with sentry_sdk.traces.start_span(name="span in trace 2"):
# This span is the root of a new, separate trace
...
See Custom Trace Propagation for more information on distributed tracing.
To modify or redact span data before it's sent, use the before_send_span option:
import sentry_sdk
def postprocess_span(span, hint):
attributes_to_sanitize = [
"http.request.header.custom-auth",
"http.request.header.custom-user-id",
]
for attribute in attributes_to_sanitize:
if span["attributes"].get(attribute):
span["attributes"][attribute] = "[Sanitized]"
return span
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
trace_lifecycle="stream",
before_send_span=postprocess_span,
)
import sentry_sdk
def postprocess_span(span, hint):
attributes_to_sanitize = [
"http.request.header.custom-auth",
"http.request.header.custom-user-id",
]
for attribute in attributes_to_sanitize:
if span["attributes"].get(attribute):
span["attributes"][attribute] = "[Sanitized]"
return span
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
trace_lifecycle="stream",
before_send_span=postprocess_span,
)
before_send_span can only modify span data — you cannot use it to drop spans (use ignore_spans instead).
To prevent specific spans from being created, use the ignore_spans option. Rules are evaluated at span start, so only the span name and attributes set at creation time are taken into account. Rules can be strings, compiled regexes, or dictionaries with name and/or attributes conditions:
import re
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
trace_lifecycle="stream",
ignore_spans=[
# String match against span name
"/health",
# Regex match against span name
re.compile(r"/flow/.*"),
# Match by attributes (all must match)
{
"attributes": {
"service.id": "15def9a",
"flow.pipeline": "legacy",
}
},
# Match by name and attributes
{
"name": re.compile(r"/flow/.*"),
"attributes": {
"service.id": re.compile(r".*\.facade"),
},
},
],
)
import re
import sentry_sdk
sentry_sdk.init(
dsn="___PUBLIC_DSN___",
traces_sample_rate=1.0,
trace_lifecycle="stream",
ignore_spans=[
# String match against span name
"/health",
# Regex match against span name
re.compile(r"/flow/.*"),
# Match by attributes (all must match)
{
"attributes": {
"service.id": "15def9a",
"flow.pipeline": "legacy",
}
},
# Match by name and attributes
{
"name": re.compile(r"/flow/.*"),
"attributes": {
"service.id": re.compile(r".*\.facade"),
},
},
],
)
If a matching span is a service span, all of its child spans are dropped as well. If a child span matches, only that span is dropped and its children are reparented to the nearest ancestor.
If you use traces_sample_rate, no changes are needed — it works the same way in stream mode.
If you use a custom traces_sampler, the shape of the sampling context is different in stream mode. See the Streamed Span API migration guide for details.
To make sure you've enabled stream mode successfully:
- Check the Sentry dashboard: Spans should appear in the Traces view shortly after they complete. Traces look similar to transaction mode, but contain only spans and no transactions.
- Check your logs: If the SDK logs warnings about unsupported span operations, you may still be using the legacy Span API somewhere in your code.
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").