Skip to content

Sentry.metrics.* triggers setTimeout outside request context in Cloudflare Workers #20888

Closed

Description

@sergical

Problem

Sentry.metrics.* (count, gauge, distribution) is exported and enabled by default in @sentry/cloudflare, but capturing a metric triggers a setTimeout(..., 5000) that fires outside the request context, causing a Cloudflare Workers runtime error.

This is particularly visible when bundling with @cloudflare/vite-plugin (which applies stricter runtime checks than older Wrangler builds).

Root Cause

The core client's setupWeightBasedFlushing (packages/core/src/client.ts:105-158) registers an afterCaptureMetric hook that schedules a 5-second setTimeout (DEFAULT_FLUSH_INTERVAL = 5000) to batch-flush the metric envelope. In Cloudflare Workers, this timer fires after the request handler returns, violating the request-scoped execution model.

// packages/core/src/client.ts:139-152
} else if (!isTimerActive) {
  isTimerActive = true;
  flushTimeout = safeUnref(
    setTimeout(() => {
      flushFn(client);
    }, DEFAULT_FLUSH_INTERVAL),
  );
}

The CloudflareClient.flush() method handles pending spans but does not address the metric flush timer created by the base client.

Current Workarounds

  1. Disable standalone metrics and use span attributes/span metrics instead:
    Sentry.init({ enableMetrics: false });
  2. Call Sentry.flush() at the end of every request — this triggers the flushMetrics hook which clears the timer, but the timer still gets created before being cancelled.

Possible Fixes

  • Make DEFAULT_FLUSH_INTERVAL configurable (e.g. metricsFlushInterval option), allowing @sentry/cloudflare to set it to 0 for synchronous flushing.
  • Have @sentry/cloudflare override the flushing strategy to flush synchronously within the request context rather than relying on background timers.
  • At minimum, document that Sentry.metrics.* is not fully supported in Workers and recommend span metrics.

Context

  • Related to [Metrics] Support cloudflare workers #11220 (closed in 2024 as "won't do" when the old metrics.increment()/metrics.set() API was replaced with span-attribute-based metrics). However, Application Metrics (Sentry.metrics.count/gauge/distribution) is a newer product that shipped after that decision, so the closure rationale no longer fully applies.
  • Span metrics migration docs

Package

@sentry/cloudflare

Activity

  1. linear-code commented on May 14, 2026

    @linear-code
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on May 14, 2026
  3. sergical commented on May 14, 2026

    @sergical
    MemberAuthor

    Closing this — after tracing through the code, withSentry creates the client inside the request handler (wrapRequestHandler → init()), so the setTimeout from metric capture runs within request context. The timer is also cleared by flushAndDispose() at end of request via waitUntil.

    We need the actual stack trace from the original reporter to understand what's happening in their setup. Reopening if/when we get that.

  4. sergical commented on May 14, 2026

    @sergical
    MemberAuthor

    Reopening with updated understanding. The setTimeout from setupWeightBasedFlushing is called within request context (inside the afterCaptureMetric hook), but workerd rejects it when the Worker is built with @cloudflare/vite-plugin using no_bundle: true + native ESM multi-module output. The same code works with Wrangler's single-bundle output.

    Stack trace from production confirms the timer originates from the afterCaptureMetric hook → client.emit → setTimeout:

    assets/worker-entry-CFAEviHj.js:19:20644 (UT)
    assets/worker-entry-CFAEviHj.js:9:26402 (D3)
    assets/worker-entry-CFAEviHj.js:9:34015 (ba.emit)
    <anonymous> (Set.forEach)
    assets/worker-entry-CFAEviHj.js:9:34026
    assets/worker-entry-CFAEviHj.js:9:33932 (o)
    assets/worker-entry-CFAEviHj.js:9:30527
    node-internal:internal_timers:104:12 (setTimeout)
    node-internal:internal_timers:52:43 (new Timeout)
    node-internal:internal_timers:57:15 (#constructTimer)
    

    This likely also affects afterCaptureLog (same setupWeightBasedFlushing mechanism).

    Proposed fix: Add a _flushInterval client option. Set to 0 in @sentry/cloudflare (and @sentry/vercel-edge) to skip the timer entirely. Size-based flush (800KB) and explicit flush() at end of request (via withSentry → flushAndDispose) still work. Zero performance impact since each request creates its own client.

  5. logaretm commented on May 14, 2026

    @logaretm
    Member

    @JPeer264 Can u take a look at this and #20889

  6. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on May 14, 2026
  7. added a commit that references this issue on May 20, 2026
    68155d7
  8. added a commit that references this issue on May 20, 2026
    95b909e
  9. github-actions commented on May 26, 2026

    @github-actions
    Contributor

    A PR closing this issue has just been released 🚀

    This issue was referenced by PR #20889, which was included in the 10.54.0 release.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions