---
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/dart/tracing/streamed-spans/
---

# Streamed Spans | Sentry for Dart

By default, the Sentry 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, instead of waiting for the whole transaction to complete.

Why use 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 as they finish.
* **Lower memory usage.** Spans are flushed as they complete instead of being held in memory until the root span ends. This is especially useful for long-lived screens and background isolates.
* **Faster visibility.** Span data arrives in Sentry as your app runs, instead of only after the entire operation completes.
* **Fewer spans lost to crashes.** If your app terminates unexpectedly, spans that were already flushed are still delivered. In transaction mode, a crash before the transaction ends means all of its span data is lost.

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 sampling decisions are made here.
* **Service span**: The top-level span within a service boundary. It has no local parent, but it can have a remote parent in another service. Pass `parentSpan: null` to create one. It's the stream mode equivalent of a transaction. The first service span in a trace is also its root span.
* **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
```

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

You need:

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

## [Migrate from Transaction Mode](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#migrate-from-transaction-mode)

For most apps, switching to stream mode requires no code changes beyond the initial opt-in. Automatic instrumentation switches to the streaming span APIs for you.

If you use custom instrumentation or transaction-specific configuration, follow these steps:

1. [Enable stream mode](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#enable-stream-mode).
2. Replace `Sentry.startTransaction` and `ISentrySpan.startChild` with the [streaming span APIs](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#start-a-span).
3. Replace span data and tags with [attributes](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#add-attributes).
4. Replace `beforeSendTransaction` and `ignoreTransactions` with [`beforeSendSpan`](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#filter-spans) and [`ignoreSpans`](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#drop-spans).
5. [Verify the migration](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#verify-your-setup).

See the [Migration Guide](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md) for complete before-and-after examples.

### [Agent-Assisted Migration](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#agent-assisted-migration)

Copy the following prompt and paste it into your AI agent:

```txt
Use curl to download, read and follow https://raw.githubusercontent.com/getsentry/sentry-for-ai/refs/heads/main/skills-legacy/sentry-span-streaming-dart/SKILL.md to enable and migrate to span streaming in the Sentry SDK.
```

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

Opt in by setting `traceLifecycle` to `SentryTraceLifecycle.stream` when initializing the SDK. This is the only required config change:

```dart
import 'package:sentry/sentry.dart';

Future<void> main() async {
  await Sentry.init((options) {
    options.dsn = 'https://<key>@o<orgId>.ingest.sentry.io/<projectId>';
    options.tracesSampleRate = 1.0;
    // Enables stream mode
    options.traceLifecycle = SentryTraceLifecycle.stream;
  });
}
```

To revert to transaction mode, remove the option or set `traceLifecycle` to `SentryTraceLifecycle.static` (the default).

Use only the APIs for the tracing mode you choose. Calls to APIs from the other mode are ignored:

* In `stream` mode, transaction APIs (`Sentry.startTransaction`, `ISentrySpan.startChild`) are ignored.
* In `static` mode, the new span APIs (`Sentry.startSpan`, `Sentry.startSpanSync`, and `Sentry.startInactiveSpan`) are ignored.

Auto-instrumentations switch to the correct API automatically based on this setting.

How does span flushing work?

When stream mode is enabled, 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.
* When the internal buffer size exceeds 1 MiB.

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/dart/tracing/streamed-spans.md#manual-instrumentation-optional)

The SDK instruments common operations for you, but you can wrap your own code in spans to measure anything that matters to your app.

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

Use `Sentry.startSpan` to create a span that ends automatically when the callback completes:

```dart
final result = await Sentry.startSpan(
  'my-operation',
  (_) async {
    // Your code here
    return await doWork();
  },
  attributes: {
    'my.attribute': SentryAttribute.string('value'),
  },
);
```

Child spans created inside the callback are automatically associated with the parent through zones:

```dart
await Sentry.startSpan('parent-operation', (_) async {
  await Sentry.startSpan('child-step-1', (_) async {
    await stepOne();
  });

  await Sentry.startSpan('child-step-2', (_) async {
    await stepTwo();
  });
});
```

By default, a span inherits the currently active span as its parent. To change this, pass `parentSpan`:

* `parentSpan: null` forces a service span with no local parent.
* `parentSpan: someSpan` parents the new span under a specific span.

```dart
// force a new service span
await Sentry.startSpan(
  'checkout-flow',
  (span) async {
    await runCheckout();
  },
  parentSpan: null,
);

// start a child span with a specific parent
final parent = Sentry.startInactiveSpan(
  'background-sync',
  parentSpan: null,
);

try {
  await someOtherAsyncBoundary();

  await Sentry.startSpan(
    'fetch-page',
    (child) async {
      await api.fetchPage();
    },
    parentSpan: parent,
  );
} finally {
  parent.end();
}
```

#### [Create Spans for Synchronous Work](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#create-spans-for-synchronous-work)

`Sentry.startSpan` takes an asynchronous callback (returning `Future<T>`). For synchronous work, use `Sentry.startSpanSync`, which takes a synchronous callback (returning `T`). Both variants can be freely nested, and parent-child relationships resolve correctly across sync and async boundaries:

```dart
final config = Sentry.startSpanSync('parse-config', (_) {
  return Config.parse(raw);
});
```

If a span isn't sampled, the callback still runs and receives a no-op span, so all span operations remain safe to call.

#### [Create Spans That Outlive a Callback](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#create-spans-that-outlive-a-callback)

Use `Sentry.startInactiveSpan` when the work can't be wrapped in a single callback — widget lifecycles, stream subscriptions, or platform channel round-trips. You have to call `end()` manually, and other spans do **not** automatically become its children — to nest a span under it, pass it explicitly via `parentSpan` when starting the child.

```dart
final paymentSpan = Sentry.startInactiveSpan(
  'payment',
  attributes: {'payment.provider': SentryAttribute.string('stripe')},
);

// ...later, from a different entry point
void onPaymentComplete() {
  paymentSpan.end();
}
```

#### [Retroactively Set Span Timing](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#retroactively-set-span-timing)

When the real start or end of the work happened before you could create or end the span (for example, a duration measured by a platform channel) pass `startTimestamp` or an explicit `end` time:

```dart
// startTimestamp is available on the callback variants
Sentry.startSpanSync('replay-import', (_) => importRows(),
    startTimestamp: measuredStart);

final paymentSpan = Sentry.startInactiveSpan('payment');
// ...native reports the work ended at `nativeEnd`
paymentSpan.end(endTimestamp: nativeEnd);
```

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

Attach structured metadata to spans using typed `SentryAttribute` values.

You can set attributes when starting a span:

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/).

```dart
await Sentry.startSpan(
  'process-order',
  (_) async {
    await processOrder();
  },
  attributes: {
    'sentry.op': SentryAttribute.string('queue.process'),
    'order.id': SentryAttribute.string('abc-123'),
    'order.item_count': SentryAttribute.int(5),
    'order.priority': SentryAttribute.bool(true),
  },
);
```

Or add them to an already running span with `setAttribute` or `setAttributes`. Use `removeAttribute` to remove an attribute:

```dart
await Sentry.startSpan('handle-request', (span) async {
  span.setAttribute(
    'http.response.status_code',
    SentryAttribute.int(200),
  );

  span.setAttributes({
    'http.route': SentryAttribute.string('/api/users'),
    'user.id': SentryAttribute.string('user-42'),
  });

  await handleRequest();
});
```

SentryAttribute Factory

Each attribute value is created with a typed `SentryAttribute` factory:

| Factory                          | Dart Type      |
| -------------------------------- | -------------- |
| `SentryAttribute.string(v)`      | `String`       |
| `SentryAttribute.int(v)`         | `int`          |
| `SentryAttribute.bool(v)`        | `bool`         |
| `SentryAttribute.double(v)`      | `double`       |
| `SentryAttribute.stringArray(v)` | `List<String>` |
| `SentryAttribute.intArray(v)`    | `List<int>`    |
| `SentryAttribute.boolArray(v)`   | `List<bool>`   |
| `SentryAttribute.doubleArray(v)` | `List<double>` |

### [Shared Attributes](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#shared-attributes)

In transaction mode, tags set on the scope are applied to the transaction. In stream mode, tags aren't applied to spans. You don't need to remove existing tags because they still apply to error events, but you should add attributes for data that's also relevant to spans.

Use `Sentry.setAttributes` to attach attributes to the current scope. The SDK automatically includes them on spans created from that scope:

```dart
Sentry.setAttributes({
  'org_id': SentryAttribute.string(user.orgId),
  'user_tier': SentryAttribute.string(user.tier),
  'service': SentryAttribute.string('checkout'),
});
```

### [Set Span Status](https://docs.sentry.io/platforms/dart/tracing/streamed-spans.md#set-span-status)

Error handling is automatic: if the callback throws (or its future errors), the span status is set to `error` before the span ends and the error is rethrown. Otherwise the status defaults to `ok`.

Status can only be `SentrySpanStatusV2.ok` or `SentrySpanStatusV2.error`:

```dart
await Sentry.startSpan('sync', (span) async {
  if (!await isReachable()) {
    span.status = SentrySpanStatusV2.error;
    return;
  }
  await sync();
});
```

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

You can shape what ends up in Sentry by filtering span data or dropping spans entirely.

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

To modify or redact span data before it's sent, use `beforeSendSpan`:

```dart
options.beforeSendSpan = (span) {
  span.removeAttribute('http.request.body');
};
```

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

To prevent specific spans from being sent, use `ignoreSpans`. Rules are evaluated at span start and match against the span name. Attribute-based matching is not yet supported.

```dart
options.ignoreSpans = [
  IgnoreSpanRule.nameEquals('health-check'),
  IgnoreSpanRule.nameStartsWith('internal.'),
  IgnoreSpanRule.nameContains('metrics'),
  IgnoreSpanRule.nameEndsWith('.bg'),
];
```

IgnoreSpanRule Factory

| Factory                                  | Matches                           |
| ---------------------------------------- | --------------------------------- |
| `IgnoreSpanRule.nameEquals(String)`      | Exact span name                   |
| `IgnoreSpanRule.nameStartsWith(Pattern)` | Name prefix (String or RegExp)    |
| `IgnoreSpanRule.nameContains(Pattern)`   | Name substring (String or RegExp) |
| `IgnoreSpanRule.nameEndsWith(String)`    | Name suffix                       |

When an ignored span has children, the children are re-parented to the nearest recording ancestor rather than dropped.

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

If you use `tracesSampleRate`, no changes are needed — it works the same way in stream mode.

If you use a custom `tracesSampler`, the shape of the sampling context is different in stream mode. Instead of a transaction context, read the span's `name` and `attributes` from `samplingContext.spanContext`:

```dart
options.tracesSampler = (samplingContext) {
  final spanContext = samplingContext.spanContext;
  if (spanContext.name == 'health-check') {
    return 0.0;
  }
  return 0.2;
};
```

Only service spans are sampled and child spans inherit the service span's decision. When a service span isn't sampled, its callback still executes with a no-op span, so all span operations remain safe to call.

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

Automatic trace propagation for outgoing requests continues to work in stream mode.

See [Trace Propagation](https://docs.sentry.io/platforms/dart/tracing/trace-propagation.md) for configuration and supported integrations.

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

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 spans instead of transactions. Spans are buffered briefly and flushed in batches, so expect a short delay before they appear.
* **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. See the [Migration Guide](https://docs.sentry.io/platforms/dart/tracing/streamed-spans/migration-guide.md) to update it.

## Pages in this section

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