Build1 distinct publisher3 min readPublished
Instrumentation code that passes under next dev can still mislabel production traffic, because one warm instance serves many concurrent requests and a module-level variable has room for exactly one of them.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Start with the storage. A module-scope `let` is one slot, allocated when the instance loads the module, and Next.js loads `instrumentation.ts` before any other application module [1]. `register()` fires at that same moment, once [2]. Every request that lands on the instance afterwards shares that one slot [7]. So you have one slot per instance and one required value per in-flight request, which means the assignment is only correct while exactly one request is running [1].
The runtime behaviour is ordinary. Request A writes its ID and awaits something. Request B writes over the slot. A resumes and reads B's value. The field notes describe this as shared mutable state across every request currently running on the instance, with interleaving requests stomping on each other's value [15].
Which is why the local result does not transfer. `next dev` rarely has more than one request in flight against the same process [9], so a green local run establishes one thing: concurrency was one. For the pass to mean anything in production you would need one request per instance there too, and reusing the instance is the point of the platform's design [7]. Development is a load test with a sample size of one.
The correct store is already in the process. OpenTelemetry's SDK carries request context in `AsyncLocalStorage`, and per the same notes that is the mechanism that actually keeps a value scoped to one request [10]. The cost is the one the author was avoiding when he reached for the global: the user ID has to be threaded through the call layers, five of them in his case, or pulled back out of that context at the point of use [16].
Adoption cost on the tracing side is genuinely low, and the packaging is good work. `registerOTel({ serviceName })` from `@vercel/otel` covers exporter selection, batch span processor configuration and resource attributes in one call [4]. By hand you are choosing a span processor, a context manager and an exporter, and getting the registration order right before any other module loads [5]. Point `OTEL_EXPORTER_OTLP_ENDPOINT` at a collector and Honeycomb, Axiom, Datadog or a self-hosted OpenTelemetry Collector will take the spans [11]; on Vercel, the platform's own Observability tab picks up the same data once `@vercel/otel` is registered, which covers a lot of debugging without a separate backend [12]. The full Node OpenTelemetry SDK needs the Node.js runtime, which Fluid Compute makes the practical default there anyway, and the older edge runtime was never a good host for a stateful SDK [13]. If your setup snippet came from a Next.js 13 or 14 tutorial, delete the `experimental.instrumentationHook` flag, because the behaviour has been stable since 15 [3].
One more thing worth budgeting for: a trace, a structured log and a metric each answer a different question about the same request, and exporting only one of them leaves the gaps the other two would have closed [14]. That is an argument for wiring all three, not for skipping the context work that makes any of them attributable.
My context is an app already on Vercel's Node runtime, where this is a correctness fix with a privacy blast radius rather than a performance knob. The slot arithmetic travels, though: the same failure shows up in any environment that reuses one process across concurrent requests [17].
Ranked by verification strength, evidence, and original report placement.
instrumentation.ts is a file at the project root, or inside src/, that Next.js loads before any other application module.
The register() export from instrumentation.ts 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 the instrumentation.ts behaviour is stable with no experimental.instrumentationHook flag required, which trips people copying setup snippets from Next.js 13 or 14 tutorials.
@vercel/otel wraps the standard @opentelemetry/sdk-node setup, including exporter selection, batch span processor configuration and resource attributes such as service name, behind a single registerOTel() call.
Without @vercel/otel, wiring 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.
onRequestError, stable since Next.js 15, catches errors from Server Components, Route Handlers, Server Actions and Middleware before Next.js renders its own error response, whereas a component-level try/catch only sees its own subtree.
Distinct publishers with included, body-backed reporting in this cluster.
Follow any of these and your For You feed starts watching them — no settings page required.
build
Your Next.js rate limiter counts per instance, and Server Actions hide behind the page URL1 distinct publisher
build
With CRA out of React's docs, the new project default is a rendering decision1 distinct publisher
build
The MCP test that matters: a log tool that fetched the data and then said it failed1 distinct publisher
build
Instrumentation is solved. The bill for storing what it collects is not.1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
One practitioner, one checkable mechanism
Everything here comes from a single dev.to post by one developer — no Vercel engineering note, no Next.js release page, no second team reporting mislabeled traces. What rescues it is that the central argument needs no trust: a module-level variable holds exactly one value, and an instance serving several requests concurrently writes to that slot several times. The assertions that cannot be checked that way — automatic ingestion by Vercel's Observability tab, the precise ordering of onRequestError against Next.js's own error response — are documentation-shaped statements repeated from memory.
A single production account
The measurable uptake is one deployment: the author's own, under what they call real concurrent traffic, with wrong user IDs on log lines as the symptom. No request volume, no duration, no count of affected users, no second team saying the same thing. The exposed surface is admittedly wide — the instrumentation hooks are generally available and Fluid Compute's instance reuse is the default this advice is written against — but breadth of exposure is not breadth of reported experience.
Undersold for a cross-user leak
The framing is smaller than the finding. A piece presented as field notes on tracing hooks buries the part where production logs attributed one person's activity to another — and log lines feed support tooling, incident review and sometimes audit trails, where a wrong identity is worse than a missing one. Nothing is inflated: the fix is a few lines of AsyncLocalStorage and it is offered as exactly that.
Nothing disclosed, conclusion Vercel-shaped
The author sells nothing and declares nothing, which is the ordinary shape of a practitioner postmortem. Still worth naming that the recommended route runs through Vercel's own package and terminates in Vercel's own Observability tab, while Honeycomb, Axiom and Datadog get one line and the vendor-neutral manual setup appears mainly as the boilerplate you are spared. Convenience explains that ordering as easily as interest does — but no affiliation is stated either way.
Solid on mechanism, blind on magnitude
The concurrency reasoning holds on its own terms, so the how is not in doubt. The how much is entirely out of reach: one publisher, one author, one anecdote, and no way from here to judge whether this is a rare mistake or something quietly wrong across many Next.js apps on warm instances. Traffic, duration and the downstream consumers of those mislabeled logs are all absent.