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 JavaScript SDKs collect all spans in memory and send 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
Span stream mode will be enabled by default in version 11.0.0 of the SDK. You can already opt into stream mode in version 10 by following the migration guide below.
You need:
- Tracing configured in your app
@sentry/nodeSDK version>=10.66.0
For most users, switching to stream mode requires no code changes beyond the initial opt-in. If you use beforeSendSpan or beforeSendTransaction, follow these steps:
- Enable stream mode
- Wrap
beforeSendSpanwithSentry.withStreamedSpan()to filter spans - Replace
beforeSendTransactionwithignoreSpansto drop spans - Migrate tags (
Sentry.setTag(s)) to attributes (Sentry.setAttribute(s)) - Verify the migration
Copy the following prompt and paste it into your AI agent:
Follow ___CURRENT_URL___ to enable and migrate to span streaming in the Sentry SDK.
Follow ___CURRENT_URL___ to enable and migrate to span streaming in the Sentry SDK.
Opt in by setting the traceLifecycle option to 'stream' when initializing the SDK:
instrument.jsconst Sentry = require("@sentry/node");
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
// enables stream mode
traceLifecycle: "stream",
});
const Sentry = require("@sentry/node");
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
// enables stream mode
traceLifecycle: "stream",
});
import * as Sentry from "@sentry/node";
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
// enables stream mode
traceLifecycle: "stream",
});
To revert to transaction mode, set traceLifecycle to 'static' (the default) or remove the option entirely.
Mixing tracing modes in distributed tracing
Tracing modes are scoped per SDK, which means you can use, for example, stream mode in your frontend, and transaction mode in your backend, or vice versa.
Use Sentry.startSpan() to create a span that is automatically ended when the callback completes:
const result = await Sentry.startSpan(
{ name: "my-operation", attributes: { "my.attribute": "value" } },
async () => {
// Your code here
return await doWork();
},
);
const result = await Sentry.startSpan(
{ name: "my-operation", attributes: { "my.attribute": "value" } },
async () => {
// Your code here
return await doWork();
},
);
Child spans created inside the callback are automatically associated with the parent:
await Sentry.startSpan({ name: "parent-operation" }, async () => {
await Sentry.startSpan({ name: "child-step-1" }, async () => {
await stepOne();
});
await Sentry.startSpan({ name: "child-step-2" }, async () => {
await stepTwo();
});
});
await Sentry.startSpan({ name: "parent-operation" }, async () => {
await Sentry.startSpan({ name: "child-step-1" }, async () => {
await stepOne();
});
await Sentry.startSpan({ name: "child-step-2" }, async () => {
await stepTwo();
});
});
For more details on span creation APIs, such as startSpan, startSpanManual, or startInactiveSpan, see Instrumentation.
Attach structured metadata to spans using attributes, which can be string, number, or boolean, as well as arrays of these types.
You can set attributes when starting a span:
Sentry.startSpan(
{
name: "process-order",
attributes: {
"sentry.op": "queue.process",
"order.id": "abc-123",
"order.item_count": 5,
"order.priority": true,
},
},
() => {
// Process the order
},
);
Sentry.startSpan(
{
name: "process-order",
attributes: {
"sentry.op": "queue.process",
"order.id": "abc-123",
"order.item_count": 5,
"order.priority": true,
},
},
() => {
// Process the order
},
);
Or add them to an already running span:
Sentry.startSpan({ name: "handle-request" }, (span) => {
// Set a single attribute
span.setAttribute("http.response.status_code", 200);
// Set multiple attributes at once
span.setAttributes({
"http.route": "/api/users",
"user.id": "user-42",
});
});
Sentry.startSpan({ name: "handle-request" }, (span) => {
// Set a single attribute
span.setAttribute("http.response.status_code", 200);
// Set multiple attributes at once
span.setAttributes({
"http.route": "/api/users",
"user.id": "user-42",
});
});
Find more examples in our Sending Span Metrics documentation.
Previously, transaction mode applied shared tags (Sentry.setTag(s)) to the service span (transaction). In Stream mode, tags are no longer applied to spans. Set shared attributes on a specific scope instead. You don't need to remove tags from your code, since they still apply to errors. Instead, add attributes for all data that's relevant for spans, logs metrics.
Use Sentry.setAttribute and Sentry.setAttributes to attach attributes that are automatically included in all spans (as well as your logs and metrics). These work just like Sentry.setTag and Sentry.setTags, but they accept string, number, and boolean values.
To attach attributes to a broader or narrower context, set them on a specific scope instead. Use the global scope for app-wide attributes and the current scope for a single operation.
See Attributes for more information.
// Applied to all spans, logs and metrics
Sentry.setAttributes({
org_id: user.orgId,
user_tier: user.tier,
});
Sentry.setAttribute("service", "checkout");
// Global scope - shared across entire app
Sentry.getGlobalScope().setAttributes({
service: "checkout",
version: "2.1.0",
});
// Current scope - single operation
Sentry.withScope((scope) => {
scope.setAttribute("request_id", req.id);
Sentry.logger.info("Processing order");
});
// Applied to all spans, logs and metrics
Sentry.setAttributes({
org_id: user.orgId,
user_tier: user.tier,
});
Sentry.setAttribute("service", "checkout");
// Global scope - shared across entire app
Sentry.getGlobalScope().setAttributes({
service: "checkout",
version: "2.1.0",
});
// Current scope - single operation
Sentry.withScope((scope) => {
scope.setAttribute("request_id", req.id);
Sentry.logger.info("Processing order");
});
To modify or redact span data before it's sent, use beforeSendSpan. In stream mode, wrap it with Sentry.withStreamedSpan() so the SDK applies it to spans as they are flushed rather than only at transaction time.
beforeSendSpan can only modify span data, and you cannot use it to drop spans. Use ignoreSpans instead.
The span object also has different property names in stream mode. For example, span.op becomes span.attributes?.["sentry.op"] and span.description becomes span.name. See the migration note below for the full list.
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
traceLifecycle: "stream",
beforeSendSpan: Sentry.withStreamedSpan((span) => {
// In stream mode, 'op' is accessed via attributes
if (span.attributes?.["sentry.op"] === "db.query") {
// In stream mode, 'description' is now renamed to 'name'
span.name = "[filtered]";
}
return span;
}),
});
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
traceLifecycle: "stream",
beforeSendSpan: Sentry.withStreamedSpan((span) => {
// In stream mode, 'op' is accessed via attributes
if (span.attributes?.["sentry.op"] === "db.query") {
// In stream mode, 'description' is now renamed to 'name'
span.name = "[filtered]";
}
return span;
}),
});
In stream mode, ignoreSpans is evaluated at span start, so only the span name and attributes available at that point are taken into account. Any name updates or additional attributes added while the span is active won't influence whether the span is dropped.
To prevent specific spans from being created, use the ignoreSpans option:
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
traceLifecycle: "stream",
ignoreSpans: [
// Drop spans whose name contains "healthcheck"
"healthcheck",
// Drop spans whose name matches a pattern
/^GET \/api\/v1\/internal/,
// Drop spans matching name and attribute conditions
{
name: /^GET \//,
attributes: {
"http.route": "/api/status",
},
},
],
});
Sentry.init({
dsn: "___PUBLIC_DSN___",
tracesSampleRate: 1.0,
traceLifecycle: "stream",
ignoreSpans: [
// Drop spans whose name contains "healthcheck"
"healthcheck",
// Drop spans whose name matches a pattern
/^GET \/api\/v1\/internal/,
// Drop spans matching name and attribute conditions
{
name: /^GET \//,
attributes: {
"http.route": "/api/status",
},
},
],
});
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.
Migrating from transaction mode?
In transaction mode, ignoreSpans is evaluated at transaction end rather than at span start. Review your existing rules to make sure the attributes and names you're matching on are passed when the span is created.
If you're auto-instrumenting and don't know what the initial name of a span is when it starts, enable SDK debug logging during development by setting debug: true when initializing the SDK.
Distributed tracing works out of the box when tracing is enabled and works the same way in stream mode. If you need to manually propagate trace context, for example, when the SDK can't instrument automatically, see Custom Trace Propagation.
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 the same as in transaction mode, but without transactions.
- Check for fallback warnings in your logs: If the SDK logs warnings about falling back to transaction mode, your
beforeSendSpancallback is likely missing theSentry.withStreamedSpan()wrapper.
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").