Skip to content

vercelWaitUntil() is a no-op on Vercel's Node.js runtime, silently dropping events flushed by the Next.js wrappers #23087

Description

@alieruuu

Summary

vercelWaitUntil() returns early on anything that is not the Edge runtime. Because every Next.js SDK wrapper flushes through it, server-side events captured by those wrappers are silently dropped on Vercel's Node.js runtime (Fluid Compute) — the function freezes before the envelope is sent.

There is no error, no warning, and no debug output. captureException runs, the event is queued, and nothing ever leaves the process.

Versions

  • @sentry/nextjs 10.69.0
  • next 16.2.11 (Turbopack build, the default in Next 16)
  • Vercel Functions, Node.js runtime (Fluid Compute), nodejs22.x

The code

@sentry/core/build/cjs/utils/vercelWaitUntil.js:

function vercelWaitUntil(task) {
  if (typeof EdgeRuntime !== "string") {
    return;                       // <-- Node.js runtime returns here
  }
  const vercelRequestContextGlobal =
    worldwide.GLOBAL_OBJ[Symbol.for("@vercel/request-context")];
  const ctx = vercelRequestContextGlobal?.get?.();
  if (ctx?.waitUntil) {
    ctx.waitUntil(task);
  }
}

@sentry/nextjs/build/cjs/common/utils/responseEnd.js routes every flush through it:

function waitUntil(task) {
  if (isCloudflareWaitUntilAvailable()) {
    cloudflareWaitUntil(task);
    return;
  }
  core.vercelWaitUntil(task);
}

And both App Router wrappers end with it — wrapRouteHandlerWithSentry:

() => {
  responseEnd.waitUntil(responseEnd.flushSafelyWithTimeout());
}

wrapServerComponentWithSentry does the same.

So on Vercel + Node.js runtime, the flush is a no-op and the queued envelope dies with the frozen instance.

Reproduction

  1. Next.js 16 app on Vercel, Node.js runtime (the default), Turbopack build.
  2. Wrap an App Router route handler with wrapRouteHandlerWithSentry and throw from it.
  3. The error never appears in Sentry.

Because Turbopack builds no longer apply build-time instrumentation, we were applying the wrappers manually, but the same code path runs when the SDK applies them itself under webpack.

Evidence that the request is never made

The Vercel trace for the invocation reports "External APIs: No outgoing requests", while the thrown error itself is present in the runtime logs. So the handler ran, the wrapper ran, and no HTTP request to Sentry was ever attempted.

Adding an explicit awaited flush around the same wrapper fixes it immediately and the event arrives with the correct mechanism: auto.function.nextjs.route_handler:

try {
  return await wrapped(...args)
} catch (err) {
  await flush(2000)
  throw err
}

A probe on the same route that called captureException followed by await flush() always delivered (afterSendEvent status=200), which isolates the problem to the missing wait rather than to transport, DSN, or initialization.

Why the guard looks wrong

EdgeRuntime is only defined on the Edge runtime, but Vercel now recommends the Node.js runtime (Fluid Compute) over Edge for most workloads, and it is the default for Next.js apps. The Node.js runtime also exposes Symbol.for("@vercel/request-context") with a working waitUntil, which is how @vercel/functions' own waitUntil operates there.

So the early return excludes the runtime where most Vercel users actually are.

Suggested fix

Drop the EdgeRuntime check and attempt the request-context lookup unconditionally; it already degrades gracefully when the symbol or waitUntil is absent:

function vercelWaitUntil(task) {
  const vercelRequestContextGlobal =
    worldwide.GLOBAL_OBJ[Symbol.for("@vercel/request-context")];
  const ctx = vercelRequestContextGlobal?.get?.();
  if (ctx?.waitUntil) {
    ctx.waitUntil(task);
  }
}

If the guard exists for a reason I am missing, it would help a lot if the no-op path emitted a debug log. Right now the failure is indistinguishable from a DSN problem, a network problem, or the wrapper not being applied at all, which is what made this take a while to find.

Possibly related

All three share the same shape: the code runs, and the event silently never leaves.

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions