Repository navigation
Span streaming (default in v11) loses all buffered spans when the Node process exits on its own #25531
Description
Is there an existing issue for this?
- I have checked for existing issues https://github.com/getsentry/sentry-javascript/issues
- I have reviewed the documentation https://docs.sentry.io/
- I am using the latest SDK release https://github.com/getsentry/sentry-javascript/releases
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/node
SDK Version
11.8.0 (latest at the time of writing)
Framework Version
Node v25.8.0, no framework
Link to Sentry event
n/a, local repro (the DSN points at a local sink on 127.0.0.1)
Reproduction Example/SDK Setup
Self-contained, only needs npm i @sentry/node. It starts a local DSN sink and runs a short
"script" as a child process twice: once with the default lifecycle and once with
traceLifecycle: 'static'. The script ends on its own without calling flush(), like a cron
job, CLI or queue worker.
// Repro: with the default span streaming, spans are lost when the process exits on its own.
// Usage: npm i @sentry/node && node exit-repro.mjs
// Everything is local (127.0.0.1); no real DSN is used.
import { spawn } from 'node:child_process';
import http from 'node:http';
import zlib from 'node:zlib';
if (process.argv[2] === 'child') {
// A short script, like a cron job or CLI: it does some work and ends without calling flush().
const Sentry = await import('@sentry/node');
Sentry.init({
dsn: `http://key@127.0.0.1:${process.argv[3]}/1`,
tracesSampleRate: 1,
...(process.argv[4] === 'static' ? { traceLifecycle: 'static' } : {}),
});
Sentry.startSpan({ name: 'cron-job' }, () => {
Sentry.startSpan({ name: 'step' }, () => {});
});
Sentry.captureMessage('job done');
} else {
// Local DSN sink: records the envelope item types it receives.
let items = [];
const sink = http.createServer((req, res) => {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => {
let body = Buffer.concat(chunks);
if (req.headers['content-encoding'] === 'gzip') body = zlib.gunzipSync(body);
for (const line of body.toString().split('\n')) {
try {
const obj = JSON.parse(line);
// Item headers have a `type`; payloads (with `event_id`) are skipped.
if (obj.type && !obj.event_id) items.push(obj.type);
} catch {}
}
res.end('{}');
});
});
await new Promise(r => sink.listen(0, '127.0.0.1', r));
const port = sink.address().port;
for (const lifecycle of ['default', 'static']) {
items = [];
await new Promise(r => spawn(process.execPath, [import.meta.filename, 'child', port, lifecycle], { stdio: 'inherit' }).on('exit', r));
await new Promise(r => setTimeout(r, 1000));
console.log(`${lifecycle.padEnd(8)} -> items received: ${JSON.stringify(items)}`);
}
sink.close();
}Steps to Reproduce
npm i @sentry/nodenode exit-repro.mjs
Expected Result
The spans are sent before the process exits, as they are with traceLifecycle: 'static' and in
v10, and as buffered logs, metrics and client reports already are.
Actual Result
default -> items received: ["event"]
static -> items received: ["transaction","event"]
Same result in 3 of 3 runs. With the default (streaming) lifecycle the error event arrives but
all spans are lost. Adding await Sentry.flush(2000) at the end delivers them
(["span","event"]), so they were only waiting in the buffer.
Additional Context
packages/node/src/sdk/client.ts:81-82register abeforeExitflush for logs and metrics,
and:209for client reports, but there is none for the span buffer. The span buffer timers
are unref'd (packages/core/src/tracing/spans/spanBuffer.ts:111,
packages/core/src/integrations/spanStreaming.ts:57), so nothing keeps the process alive
until they fire.- fix(node): Flush buffered metrics on process exit #24806 added the same
beforeExitflush for metrics ("matching the existing behavior for
logs and client reports"); spans seem to have been missed. - The node integration tests inject a
beforeExitauto-flush for every scenario (test(node): Auto-flush node-integration-tests via beforeExit #20960), which
would hide this in CI. - A graceful shutdown (SIGTERM handler that closes the server and lets the process exit) also
loses the spans of the last ~0.5 s. - Possible fix: a
beforeExitlistener that flushes the span buffer, like the ones for logs and
metrics. Happy to open a PR if that sounds right.
Metadata
Metadata
Assignees
Projects
- StatusShow more project fieldsNo status
Activity
linear-code commentedon Oct 19, 2026
JS-3960
logaretm commentedon Oct 19, 2026
Thanks for the report, got a PR out for it!