---
title: "Migrate to Stream Mode"
description: "Learn how to migrate your custom instrumentation from transaction mode to stream mode."
url: https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide/
---

# Migrate to Stream Mode | Sentry for Python

Stream mode requires the streamed Span API. If you use custom instrumentation (creating spans manually, setting span data, or filtering spans) you'll need to update that code before you can switch to stream mode. This guide walks through the changes.

For an introduction to stream mode itself, see [Streamed Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans.md).

## [Enable Stream Mode](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#enable-stream-mode)

Add the `trace_lifecycle` option when initializing the SDK:

```python
import sentry_sdk

sentry_sdk.init(
    dsn="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
+   trace_lifecycle="stream",
)
```

## [Span Creation](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-creation)

Replace `start_span`, `start_transaction`, and `start_child` with `sentry_sdk.traces.start_span()`. Whether the resulting span is a service span, a child span, or a sibling depends on the `parent_span` argument and what's currently active.

```python
import sentry_sdk

# Starting a span
- with sentry_sdk.start_span(op="http.client", description="GET /api/users") as span:
+ with sentry_sdk.traces.start_span(
+     name="GET /api/users",
+     attributes={"sentry.op": "http.client"},
+ ) as span:
      ...

# Starting what used to be a transaction: pass parent_span=None to force a service span
- with sentry_sdk.start_transaction(name="flow.checkout") as transaction:
+ with sentry_sdk.traces.start_span(name="flow.checkout", parent_span=None) as span:
      ...

# Starting a child span: just start a span while the parent is active
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}):
      ...

# Starting a child span: if the span's parent should be a span that's currently not active, you can provide it explicitly
- with parent.start_child(op="db", description="SELECT") as child:
+ with sentry_sdk.traces.start_span(name="SELECT", attributes={"sentry.op": "db"}, parent_span=parent):
      ...
```

A few argument changes come along with this:

* `description` no longer exists — use `name` instead.
* `op` is no longer a dedicated argument — set it as the `sentry.op` attribute instead.

If you use the `@trace` decorator, only the import changes:

```python
- from sentry_sdk import trace
+ from sentry_sdk.traces import trace

@trace
def checkout():
    ...
```

If your code imports `Span` or `Transaction` directly, for example for type annotations, replace both with `StreamedSpan`:

```python
- from sentry_sdk.tracing import Span, Transaction
+ from sentry_sdk.traces import StreamedSpan

- def process(span: Span) -> None:
+ def process(span: StreamedSpan) -> None:
      ...
```

## [Span Data](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-data)

In stream mode, spans have no contexts, data, or tags. Instead, everything is an attribute. Replace `set_data()`, `set_tag()`, and `set_context()` with `set_attribute()` or `set_attributes()`:

```python
- span.set_data("flow.step", "submit_payment")
- span.set_tag("http.status_code", 201)
+ span.set_attributes({
+     "flow.step": "submit_payment",
+     "http.response.status_code": 201,
+ })
```

Unlike the old methods, `set_attribute` only accepts primitive types (`str`, `int`, `float`, `bool`, or arrays of these). `None` isn't supported either. Flatten dictionaries into separate attributes, and stringify anything that can't be flattened:

```python
- span.set_data("request", {"method": "POST", "path": "/api/checkout"})
+ span.set_attributes({
+     "request.method": "POST",
+     "request.path": "/api/checkout",
+ })
```

Tags set on the scope with `sentry_sdk.set_tag()` aren't applied to spans in stream mode. Use `sentry_sdk.set_attribute()` to apply data to spans:

```python
import sentry_sdk

sentry_sdk.set_tag("region", "Europe")       # applied to errors and other tag-supporting telemetry
sentry_sdk.set_attribute("region", "Europe")  # applied to spans, logs, metrics
```

## [Accessing the Current Span](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#accessing-the-current-span)

A few ways of referencing the current span or transaction change in stream mode:

```python
import sentry_sdk

# Getting the current span
- span = sentry_sdk.get_current_span()
+ span = sentry_sdk.traces.get_current_span()

# Getting the current span via the scope
- scope = sentry_sdk.get_current_scope()
- current_span = scope.span
+ current_span = sentry_sdk.traces.get_current_span()
```

If your code reads specific fields off the trace context, access them as direct properties instead of calling `get_trace_context()`, which no longer exists on streaming spans:

```python
- ctx = span.get_trace_context()
- trace_id = ctx["trace_id"]
- span_id = ctx["span_id"]
+ trace_id = span.trace_id
+ span_id = span.span_id
```

## [Trace Propagation](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#trace-propagation)

`sentry_sdk.traces.continue_trace()` replaces the legacy `continue_trace()`. It's no longer a context manager — it sets the propagation context, and the next span you start picks it up automatically:

```python
import sentry_sdk

headers = {
    "sentry-trace": "...",
    "baggage": "...",
}

- with sentry_sdk.continue_trace(headers) as transaction:
-     ...
+ sentry_sdk.traces.continue_trace(headers)
+ with sentry_sdk.traces.start_span(name="handle request"):
+     ...
```

## [Span Status](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#span-status)

Status can only be `ok` (default) or `error` in stream mode:

```python
from sentry_sdk.traces import start_span

with start_span(name="process") as span:
    try:
        ...
    except Exception:
        span.status = "error"
```

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

If you use `traces_sample_rate`, no changes are needed.

If you use a custom `traces_sampler`, the sampling context has the same structure in stream mode, but not all keys might be populated. The `data` key under `transaction_context` contains span attributes known at span start.

```python
import sentry_sdk

def traces_sampler(sampling_context):
    if sampling_context["parent_sampled"] is not None:
        return float(sampling_context["parent_sampled"])

    if sampling_context["transaction_context"]["name"] in IGNORED_SPAN_NAMES:
        return 0.0

    return 1.0

sentry_sdk.init(
    traces_sampler=traces_sampler,
    trace_lifecycle="stream",
)
```

`custom_sampling_context` is no longer an argument to `start_span`. Set it on the scope instead, after `continue_trace` (which resets the propagation context) and before `start_span` (which is when sampling happens):

```python
import sentry_sdk

- with sentry_sdk.start_span(
-     name="handle request",
-     custom_sampling_context={"asgi_scope": asgi_scope},
- ):
-     ...
+ sentry_sdk.get_current_scope().set_custom_sampling_context({"asgi_scope": asgi_scope})
+ with sentry_sdk.traces.start_span(name="handle request"):
+     ...
```

## [Filtering and Dropping Spans](https://docs.sentry.io/platforms/python/tracing/streamed-spans/migration-guide.md#filtering-and-dropping-spans)

`before_send_transaction` has no effect in stream mode, since spans are sent individually rather than batched into a transaction. Replace it with `ignore_spans` (to drop spans) and `before_send_span` (to modify them):

```python
import re
import sentry_sdk

+ def my_span_processor(span, hint):
+     if span["attributes"].get("sentry.op") == "db.query":
+         span["name"] = "[filtered]"
+     return span

sentry_sdk.init(
    dsn="https://<key>@o<orgId>.ingest.sentry.io/<projectId>",
    traces_sample_rate=1.0,
-   before_send_transaction=my_filter,
+   trace_lifecycle="stream",
+   ignore_spans=[
+       "/health",
+       re.compile(r"^GET /api/v1/internal"),
+       {"attributes": {"service.id": "15def9a"}},
+    ],
+    before_send_span=my_span_processor,
)
```

`ignore_spans` only has access to the span name and attributes set at creation time — not attributes added later in the span's lifetime, like an HTTP status code set after the request completes. If your `before_send_transaction` logic was used to drop spans based on late-set data, this can't be replicated in stream mode. Consider server-side filtering with Sentry [inbound data filters](https://docs.sentry.io/concepts/data-management/filtering.md) or [Relay](https://docs.sentry.io/product/relay.md) rules instead. `before_send_span` runs after the span ends and has access to all attributes set during the span's lifetime.
