Skip to content

Span streaming (default in v11) loses all buffered spans when the Node process exits on its own #25531

Open

Description

@4ktLuffy

Is there an existing issue for this?

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

  1. npm i @sentry/node
  2. node 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-82 register a beforeExit flush for logs and metrics,
    and :209 for 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 beforeExit flush for metrics ("matching the existing behavior for
    logs and client reports"); spans seem to have been missed.
  • The node integration tests inject a beforeExit auto-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 beforeExit listener that flushes the span buffer, like the ones for logs and
    metrics. Happy to open a PR if that sounds right.

Activity

  1. linear-code commented on Oct 19, 2026

    @linear-code
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Oct 19, 2026
  3. self-assigned this
    on Oct 19, 2026
  4. logaretm commented on Oct 19, 2026

    @logaretm
    Member

    Thanks for the report, got a PR out for it!

  5. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions