deniz.in

Markets

Weather

Loading weather

· via dev.to (home feed)

Next.js field notes: Fluid Compute instance reuse can leak one request's state into another's trace

Field notes on dev.to explain how to wire OpenTelemetry into the Next.js App Router, and why module-level request state leaks across concurrent requests on Vercel Fluid Compute.

Next.js field notes: Fluid Compute instance reuse can leak one request's state into another's trace

What the field notes cover

A field-notes post on dev.to walks through wiring distributed tracing and centralized error reporting into a Next.js App Router application, and documents the production bug that came out of it: under real traffic, log lines and trace attributes carried the wrong user's ID, because a module-level current-user variable was shared by concurrent requests running on the same warm instance.

One boot hook, not a per-request hook

According to the post, instrumentation.ts lives at the project root or inside src/, and Next.js loads it ahead of the application's own modules. Its register() export fires exactly once, when a new server instance starts — never per request, per route, or per render. Since Next.js 15 this is stable behavior with no experimental flag, a detail that catches developers copying setup snippets written for Next.js 13 or 14. That boot-time semantics makes the file the right home for initializing a tracing SDK or opening long-lived connections, and the wrong home for anything tied to the request currently in flight.

For the SDK itself, the post uses @vercel/otel, whose registerOTel() call wraps the standard Node OpenTelemetry SDK and hides exporter selection, span-processor configuration, and resource attributes behind a single setup. Setting OTEL_EXPORTER_OTLP_ENDPOINT points spans at a collector such as Honeycomb, Axiom, Datadog, or a self-hosted OpenTelemetry Collector, and on Vercel the platform's Observability tab picks up the same trace data automatically once registration is in place. The full SDK requires the Node.js runtime, which the author calls a shrinking constraint: Fluid Compute makes Node.js the default in practice, and the older edge runtime never suited a stateful SDK.

Why request state leaked across traces

The bug came from a small helper that stashed the current user's ID in a module-level variable, avoiding the need to thread it through five layers of function calls before attaching it to a log line. The helper behaved in local development, where a next dev process seldom handles overlapping requests. Under production concurrency it attached request A's identity to request B's logs and trace attributes.

The cause, per the post, is Fluid Compute's core behavior: it keeps a single warm function instance alive and serves many concurrent requests from it rather than starting a fresh instance per request. That helps cold-start latency, but it turns a module-level variable into state shared by every request on the instance, and interleaving requests overwrite each other's values.

The fix is AsyncLocalStorage from node:async_hooks, which scopes a value to the current async call chain rather than to the module. The author points out this is the underlying primitive OpenTelemetry's context manager relies on, and it is why Next.js's own headers() and cookies() can behave per-request without developers threading a request object through the whole call stack.

The error hook that sees the whole route

onRequestError, an optional export from instrumentation.ts and stable since Next.js 15, runs whenever Next.js catches an unhandled error while rendering a Server Component, executing a Route Handler, running a Server Action, or inside Middleware — before the framework builds its own error response. It receives the error, a request object with path, method, and headers, and a context object carrying routerKind, routePath, and a routeType of render, route, action, or middleware. That route-level context is a detail a scattered set of try/catch blocks cannot supply: an error boundary inside one component only knows its own subtree, not which route it belongs to or which phase failed. The author consolidated every ad-hoc console.error across route handlers into this one hook, so nothing else in the app decides how errors are reported.

Joining logs to traces

A span ID on its own does little for whoever reads a log during an incident; the log has to carry the same trace ID as the request that produced it. Vercel's Runtime Logs capture console output automatically but do not correlate lines to traces unless the IDs are attached explicitly, and the post demonstrates a pino child logger that reads the active span context from @opentelemetry/api and stamps it onto each entry. More broadly, it argues that traces, structured logs, and metrics each capture a different facet of the same request, and shipping only one of them leaves gaps the others would fill.

Why it matters

The serverless abstraction is shifting under application code: an instance is no longer synonymous with a request, so any global, module-level cache of current state, or singleton holding request data becomes a cross-request data leak. The consequences go beyond messy logs, since one user's identity can end up attached to another user's trace. The post's guidance generalizes beyond Vercel: initialize once in register(), report errors once in onRequestError, and keep anything request-scoped inside AsyncLocalStorage — the same mechanism the OpenTelemetry SDK and Next.js itself already depend on.

  • #next-js
  • #opentelemetry
  • #observability
  • #vercel
  • #serverless

Related posts