Skip to content

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

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();
    });

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions