Observability in the Next.js App Router: Field Notes on instrumentation.ts, onRequestError, and the Trace That Leaked Across Requests

2026年8月31日1 次浏览来源:Dev.to阅读原文

Headline: Next.js's file and its hook give the App Router real distributed tracing and centralized error reporting, but Vercel Fluid Compute reusing one warm instance across concurrent requests means any request state stored outside OpenTelemetry's own context propagation — a module-level variable, a naive "current request" global — leaks between unrelated users' traces.

Key takeaways , exported from , runs once when a server instance boots, not once per request — that's exactly where a tracing SDK belongs, and exactly why request-scoped state can't live there. 's wraps the Node OpenTelemetry SDK with sane defaults for exporters and span processors, cutting most of the boilerplate a manual setup needs. , stable since Next.js 15, catches errors from Server Components, Route Handlers, Server Actions, and Middleware before Next.js renders its own error response — a component-level only ever sees its own subtree.

Fluid Compute reuses one function instance across concurrent requests, so a module-scope "current trace" variable becomes shared mutable state.

Only -based context — what the OpenTelemetry SDK already uses — keeps data scoped to a single request.

A trace, a structured log, and a metric answer different questions about the same request.

Exporting only one of them leaves gaps the other two would have closed.

What does instrumentation.ts actually run, and when? is a file at the project root — or inside — that Next.js loads before any other application module.

Its export runs exactly once, when a new server instance starts up.

It does not run per request, per route, or per render.

Since Next.js 15 this is stable behavior with no flag required, which trips people who copy setup snippets from Next.js 13 or 14 tutorials.

That single-run-per-instance timing is the whole reason this file exists: it's the correct place to initialize a tracing SDK, open a long-lived connection, or read one-time configuration, and the wrong place to store anything specific to the request currently in flight.

How do I wire up OpenTelemetry without hand-rolling the Node SDK? is a package that wraps the standard setup — exporter selection, batch span processor configuration, and resource attributes like service name — behind a single call.

Without it, wiring up the Node OpenTelemetry SDK by hand means picking a span processor, a context manager, and an exporter, and getting the instrumentation registration order right before any other module loads.

Set to point spans at a collector — Honeycomb, Axiom, Datadog, or a self-hosted OpenTelemetry Collector all accept OTLP.

On Vercel specifically, once is registered, the platform's own Observability tab picks up the same trace data automatically, which covers a lot of day-to-day debugging without standing up a separate backend.

The full Node OpenTelemetry SDK needs the Node.js runtime.

That's rarely a real constraint now — Fluid Compute makes Node.js the practical default runtime on Vercel anyway, and the older edge runtime was never a good fit for a stateful SDK like this.

Why did spans leak across requests that had nothing to do with each other?

I had a small helper that stashed the current user's ID in a module-level variable, to avoid threading it through five layers of function calls just to attach it to a log line.

It worked in local development, where rarely has more than one request in flight against the same process.

In production, under real concurrent traffic, it produced log lines and trace attributes with the wrong user's ID attached — request A's logs carrying request B's identity.

The cause is Fluid Compute's core behavior: it reuses one warm function instance to serve many concurrent requests instead of spinning up a fresh instance per request.

That's good for cold-start latency, and it's exactly why a plain module-level variable is dangerous — it's shared mutable state across every request currently running on that instance, and two requests interleaving on the same instance will stomp on each other's value.

The fix is , a Node.js primitive that scopes a value to the current async call chain rather than to the module.

It's the same mechanism OpenTelemetry's own context manager is built on, and the same reason Next.js's own and functions can be request-scoped without you passing a request object everywhere.

What does onRequestError catch that a component-level try/catch can't? is an optional export from , stable since Next.js

15.

Next.js calls it whenever it catches an unhandled error while rendering a Server Component, executing a Route Handler, running a Server Action, or inside Middleware — before it renders its own error boundary or returns a 500 response to the client.

It receives the error, a request object with , , and , and a context object with , , and (, , , or ).

That last piece is what a scattered set of blocks never gives you for free: an error boundary inside one component only knows about its own subtree, not which route it belongs to or whether it happened during a render or a Server Action.

I moved every ad-hoc I had scattered across route handlers into this one hook.

Nothing else in the app decides how errors get reported anymore.

How do I connect a trace to the log line that explains what happened?

A span ID by itself doesn't help whoever is reading a log at 2 a.m. — the log has to carry the same trace ID as the request that produced it.

Vercel's Runtime Logs capture output automatically, but they don't correlate a log line to a trace unless you attach the IDs yourself. reads the span attached to the currently active OpenTelemetry context — the same context that propagates — so this works correctly under concurrent requests for the same reason the fix above does.

Traces vs structured logs vs metrics — what should I actually export?

Each signal answers a different question, and none of them substitutes for the others.

Signal Answers Use it for Trace Where did the time go inside this one request?

A waterfall across a database call, an external API, and a render — spotting the one slow span Structured log What happened, with what data, in this one request?

Specific error payloads, business events, anything you'd grep for by request ID Metric How often, across every request?

Dashboards and alerting thresholds — error rate, p95 latency, request volume I export all three now, and the trace ID is the thing that ties a spike on a metric dashboard back to the specific log line and the specific span that explains it.

FAQ Q: Do I need the flag for in Next.js 16?

A: No.

The file and its export have been stable since Next.js 15; the experimental flag from Next.js 13 and 14 no longer exists.

Q: Does run on the Edge runtime?

A: Only a subset of it.

The full Node OpenTelemetry SDK needs the Node.js runtime, which is the practical default on Vercel now that Fluid Compute makes Node.js the standard choice.

Q: What's the difference between and a React boundary?

A: An boundary catches errors within its own component subtree and renders fallback UI for the user. is a framework-level hook that fires for errors across Server Components, Route Handlers, Server Actions, and Middleware regardless of any particular boundary — it's for reporting, not for rendering.

Q: Can I use a module-level variable instead of to pass request context around?

A: Only if the instance running your code never serves more than one request at a time.

Fluid Compute reuses instances across concurrent requests, so a module-level variable becomes shared mutable state and will leak data between unrelated requests.

Q: Do I still need a separate observability vendor if I'm on Vercel?

A: Vercel's Observability tab shows traces automatically once is registered, which covers a lot of routine debugging.

Longer retention, cross-service correlation, or custom alerting usually still means exporting via OTLP to a dedicated backend.

Originally published on devya.dev.

Also on eng-ahmed.com.

Built by Devya Solutions.

分享
Baike.dev

baike.dev helps you discover great languages, frameworks, databases, DevOps and cloud-native tools.

Quick links

About

Contribute

Found a great developer tool? Share it with the community.

Submit a tool
© 2026 baike.dev Developer EncyclopediaUpdated daily · Discover great developer tools