Skip to content

client.flush() blocks process exit for full timeout duration due to non-unref'd timers #18996

@BYK

Description

@BYK

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

10.36.0

Framework Version

Bun 1.3.3 / Node.js 22

Link to Sentry event

No response

Reproduction Example/SDK Setup

import * as Sentry from "@sentry/node";

const client = Sentry.init({
  dsn: "https://your-dsn@sentry.io/123",
  enabled: true,
  defaultIntegrations: false,
});

// This will block for 3 seconds even when there's nothing to send
await client?.flush(3000);

Steps to Reproduce

  1. Initialize Sentry in a CLI application
  2. Call client.flush(3000) before exiting
  3. Observe the process hangs for ~3 seconds even when there's nothing to flush

Expected Result

flush(timeout) should:

  1. Return immediately if there's nothing to process (_numProcessing === 0)
  2. Use unref() on internal timers so the process can exit naturally when work is complete

Actual Result

The process blocks for the full timeout duration because:

  1. _isClientDoneProcessing() in client.js checks _numProcessing after the first setTimeout, not before:

    async _isClientDoneProcessing(timeout) {
      let ticked = 0;
      while (!timeout || ticked < timeout) {
        await new Promise(resolve => setTimeout(resolve, 1));  // Always waits first
        if (!this._numProcessing) {
          return true;
        }
        ticked++;
      }
      return false;
    }
  2. The setTimeout calls don't use .unref(), keeping the Node.js event loop alive:

    • client.js line 650: setTimeout(resolve, 1) - polling loop
    • promisebuffer.js line 72: setTimeout(() => resolve(false), timeout) - drain timeout

Other parts of the codebase already use .unref() correctly (e.g., httpServerIntegration.js line 234).

Suggested Fix

  1. Add early exit check in _isClientDoneProcessing():

    async _isClientDoneProcessing(timeout) {
      if (!this._numProcessing) {
        return true;  // Early exit
      }
      // ... rest of polling loop
    }
  2. Add .unref() to timers with browser-safe check:

    await new Promise(resolve => {
      const t = setTimeout(resolve, 1);
      if (typeof t !== 'number' && t.unref) t.unref();
    });

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions