---
title: "Streamed Spans"
description: "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."
url: https://docs.sentry.io/platforms/python/tracing/streamed-spans/
---

# Streamed Spans for Python (SDK v3.x)

Version 3.0 of the Python SDK is coming in November 2026. This page describes upcoming behavior. For the SDK 2.x version of this page, select 2.x in the version selector in the sidebar.

Starting with SDK 3.0, the Sentry Python SDK sends spans to Sentry in batches as they finish. This is called stream mode. Service spans, which represent a service's entry point, replace transactions as the main grouping for each service.

In SDK 2.x, the default was transaction mode, which collects all spans in memory and sends them to Sentry as a single transaction once the root span ends. Transaction mode no longer exists in SDK 3.0.

Why stream mode?

* **No 1,000-span limit.** In transaction mode, transactions are capped at 1,000 spans. Stream mode has no upper limit since spans are sent in batches. This is especially beneficial for complex generative AI pipelines that easily exceed standard span limits.
* **Lower memory usage.** Spans are flushed periodically and don't need to be held in memory until the root span ends. This is especially useful for long-running processes like queue consumers or cron jobs.
* **Faster visibility.** Span data arrives in Sentry as your application runs, instead of only after the entire operation completes.
* **Fewer spans lost to crashes.** If your process terminates unexpectedly, spans that were already flushed are emitted normally. In transaction mode, a crash before the transaction ends means all span data is lost. Stream mode emits spans incrementally as they finish, so more of them survive a crash.

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:

```bash
Trace
│
└── Root span [service A]
     ├── Child span
     │    └── Child span
     └── Service span [service B]
          ├── Child span
          └── Child span
```

##### Upgrading from SDK 2.x?

Stream mode uses a different Span API than transaction mode. If you have existing custom instrumentation, see the [Migration Guide](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md) for a full list of changes.

## [Prerequisites](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#prerequisites)

You need:

* [Tracing configured](https://docs.sentry.io/platforms/python/tracing.md#configure) in your app
* Sentry SDK `>=3.0.0`

Stream mode is always on when tracing is enabled. No additional configuration is needed.

How does span flushing work?

The SDK maintains an internal buffer that groups spans by trace ID.

Spans are flushed:

* On a regular interval (every 5 seconds by default).
* When a trace's buffer reaches 1,000 spans.
* When the SDK shuts down.

Each flush sends only the spans accumulated since the last flush, grouped into envelopes by trace ID.

## [Manual Instrumentation (Optional)](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#manual-instrumentation-optional)

### [Start a Span](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#start-a-span)

Use `sentry_sdk.start_span()` to create a span that ends automatically when the `with` block exits:

```python
import sentry_sdk

with sentry_sdk.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:

```python
import sentry_sdk

with sentry_sdk.start_span(name="parent-operation"):
    with sentry_sdk.start_span(name="child-step-1"):
        step_one()

    with sentry_sdk.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`:

```python
import sentry_sdk

with sentry_sdk.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:

```python
from sentry_sdk import trace

@trace(name="checkout", attributes={"flow.pipeline": "legacy"})
def checkout():
    ...
```

For more details on span creation, see [Custom Instrumentation](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md).

### [Add Span Attributes](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#add-span-attributes)

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](https://getsentry.github.io/sentry-conventions/attributes/).

You can set attributes when starting a span:

```python
import sentry_sdk

with sentry_sdk.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:

```python
import sentry_sdk

with sentry_sdk.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",
    })
```

For examples, see [Adding Span Attributes](https://docs.sentry.io/platforms/python/tracing/instrumentation/custom-instrumentation.md#adding-span-attributes).

## [Breadcrumbs](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#breadcrumbs)

Breadcrumbs aren't sent with spans. They remain attached to errors, so you don't need to change how you record them.

## [Distributed Tracing (Optional)](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#distributed-tracing-optional)

### [Continue a Trace](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#continue-a-trace)

When your service receives a request from an upstream service that includes Sentry trace headers, use `sentry_sdk.continue_trace()` to connect your spans to the existing distributed trace. `continue_trace()` isn't a context manager. Instead, it sets the propagation context and the next span picks it up automatically.

```python
import sentry_sdk

headers = {
    "sentry-trace": "4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0be902b7-1",
    "baggage": "sentry-trace_id=...",
}

sentry_sdk.continue_trace(headers)
with sentry_sdk.start_span(name="handle request"):
    ...
```

### [Start a New Trace](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#start-a-new-trace)

If you need to start a completely new trace unconnected to the current one, use `sentry_sdk.new_trace()`. This is useful for background jobs or scheduled tasks where you want a clean trace boundary:

```python
import sentry_sdk

with sentry_sdk.start_span(name="span in trace 1"):
    ...

sentry_sdk.new_trace()

with sentry_sdk.start_span(name="span in trace 2"):
    # This span is the root of a new, separate trace
    ...
```

See [custom instrumentation](https://docs.sentry.io/platforms/python/tracing/distributed-tracing/custom-trace-propagation.md) for more information on distributed tracing.

## [Extended Configuration (Optional)](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#extended-configuration-optional)

### [Filter Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#filter-spans)

To modify or redact span data before it's sent, use the `before_send_span` option:

```python
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="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
    before_send_span=postprocess_span,
)
```

`before_send_span` can only modify span data — you cannot use it to drop spans (use [`ignore_spans`](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#drop-spans) instead).

### [Drop Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#drop-spans)

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:

```python
import re
import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
    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.

## [Sampling (Optional)](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#sampling-optional)

If you're upgrading from SDK 2.x and use `traces_sample_rate`, no changes are needed — it works the same way as in transaction mode.

If you use a custom `traces_sampler`, not all keys in the sampling context might be populated. See the [Streamed Span API migration guide](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md) for details.

## [Verify Your Setup](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md#verify-your-setup)

To make sure your setup works:

* **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 custom instrumentation**: If your code fails with an `ImportError` or `AttributeError` related to `sentry_sdk.tracing`, `start_transaction`, or span methods like `set_data()`, you're still using the transaction-mode Span API. See the [Migration Guide](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md) for how to update it.

## Pages in this section

- [Migrate to Stream Mode](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md)
- [Migrate to Stream Mode](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide__v2.x.md)
