Skip to content

_flush() never consumes fetch response body, causing Cloudflare Workers cross-request promise warnings #3173

@StephenTangCook

Description

@StephenTangCook

Bug description

posthog-node (via @posthog/core) triggers Cloudflare Workers cross-request promise resolution warnings because _flush() never consumes the fetch response body.

In packages/core/src/posthog-core-stateless.ts, _flush() calls fetchWithRetry() but discards the returned Response without reading the body (~line 1309):

await this.fetchWithRetry(url, fetchOptions, retryOptions)
// ↑ Response discarded — body never consumed

fetchWithRetry() (~line 1367) returns the raw response without consuming it either:

res = await this.fetch(url, { signal: ..., ...options })
if (!isNoCors && (res.status < 200 || res.status >= 400)) {
  throw new PostHogFetchHttpError(res, reqByteLength)
}
return res // body still unconsumed

In Cloudflare Workers, an unconsumed response body forces the runtime to clean up the underlying connection later, potentially in a different request's context, which triggers:

"Warning: A promise was resolved or rejected from a different request context than the one it was created in.
However, the creating request has already been completed or canceled. Continuations for that request are
unlikely to run safely and have been canceled."

This is the same class of bug the Sentry SDK had and fixed in getsentry/sentry-javascript#18534 / PR #18545.

Suggested fix: consume or cancel the response body in _flush():

// Option A: consume the body
const res = await this.fetchWithRetry(url, fetchOptions, retryOptions)
await res.text().catch(() => {})

// Option B: cancel the body stream
const res = await this.fetchWithRetry(url, fetchOptions, retryOptions)
await res.body?.cancel().catch(() => {})

Workaround — pass a custom fetch that eagerly consumes response bodies:

const cfSafeFetch: typeof fetch = async (input, init) => {
  const response = await fetch(input, init)
  const body = await response.text().catch(() => '')
  return new Response(body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers,
  })
}

new PostHog(apiKey, {
  host,
  flushAt: 1,
  flushInterval: 0,
  fetch: cfSafeFetch,
})

How to reproduce

  1. Deploy a Cloudflare Worker that uses posthog-node with flushAt: 1, flushInterval: 0 (recommended config per PostHog CF Workers docs)
  2. Call capture() inside a request handler (e.g. via executionContext.waitUntil(...) for non-blocking analytics)
  3. Send concurrent requests to the worker — the runtime warns about cross-request promise resolution from the unconsumed response bodies left by _flush()

Related sub-libraries

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin

Additional context

  • posthog-node@5.24.11 / @posthog/core@1.20.1
  • Cloudflare Workers with compatibility_date: "2026-01-12" and nodejs_compat compatibility flag
  • The warning is non-fatal but the runtime cancels continuations for the leaked promises, so post-flush error handling may silently not run
  • High-traffic workers with analytics middleware on every request generate a high volume of these warnings
  • The root cause is in packages/core/src/posthog-core-stateless.ts, so it may affect any runtime that enforces response body consumption (not just CF Workers)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions