The Problem
I upgraded a client app from Next.js 15.3 to 16.1 last Thursday. Everything built, everything deployed, and the Sentry breadcrumbs went dark within an hour. The local dev server still logged [instrumentation] register called on boot. The Vercel deployment did not. No errors, no warnings, no [instrumentation] line in the build output, no init logs on the first cold start.
instrumentation.ts had not moved. The register function was still exported. next.config.ts still had experimental.instrumentationHook: true from the 15.3 setup. The file was in the right place at the project root, sitting next to next.config.ts. The build succeeded. Sentry's Sentry.init was simply never called on the server, so every server error went unreported and every breadcrumb queue stayed empty.
What made it weird: a fresh pnpm dev locally hit the register function on every request to a server component. The exact same code, the exact same package versions, did nothing on Vercel.
Why It Happens
Next.js 16 made three changes to instrumentation that combine into a silent failure on Vercel.
First, experimental.instrumentationHook is gone. The hook is now stable and always on, but if you still have the experimental flag in next.config.ts you get no warning — Next.js silently strips the unknown experimental option and moves on. The hook itself is enabled. The difference shows up later.
Second, the register function now runs once per Node.js process on Vercel, not once per request. On the old Edge runtime fallback Next.js used to re-invoke register after cold starts at the request level, which is what made the local-vs-deployed behaviour identical. In 16, register is invoked exactly once when the serverless function boots. If your code expects to run on the request path, it never fires.
Third — and this is the one that bit me — Next.js 16 requires instrumentation.ts to live in the same directory as pages/ or app/. If your project root has a src/ folder containing app/, the instrumentation file must be at src/instrumentation.ts, not at the project root. Next.js 15 tolerated either location. Next.js 16 silently ignores the root-level file when src/app exists. The build output gives no hint.
The fourth piece is onRequestError. Next.js 16 added a new export for capturing server errors:
export async function onRequestError(err, request, context) { /* ... */ }
If your instrumentation code was relying on the global process.on('uncaughtException') pattern to catch route-handler errors, that path was narrowed in 16 to align with the new hook. Errors thrown inside server actions and route handlers no longer surface through uncaughtException — they go to onRequestError instead.
The Next.js 16 instrumentation docs cover the new hook but do not flag the src/ location requirement explicitly.
The Fix
Step 1: Drop the experimental flag. Open next.config.ts and remove experimental.instrumentationHook. Leaving it does no harm at runtime but the warning suppression is what masked the original bug:
import type { NextConfig } from 'next';
const config: NextConfig = {
// delete this whole block if you have it:
// experimental: { instrumentationHook: true },
};
export default config;
Step 2: Move the file into src/ if your app lives there. From the project root:
# if you have src/app or src/pages
mv instrumentation.ts src/instrumentation.ts
If you have both app/ at the root and src/, Next.js picks the root. If you only have src/app/, it must be at src/instrumentation.ts. Build locally with next build and grep the output:
✓ Compiled instrumentation (server)
That line confirms the file is being picked up. No line, no instrumentation.
Step 3: Split runtime-specific code. register runs once at boot, so anything that needs Node.js APIs must check the runtime before importing:
// src/instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const Sentry = await import('@sentry/nextjs');
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.VERCEL_ENV ?? 'development',
});
console.log('[instrumentation] node init complete');
}
if (process.env.NEXT_RUNTIME === 'edge') {
const Sentry = await import('@sentry/nextjs');
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
environment: process.env.VERCEL_ENV ?? 'development',
});
}
}
export async function onRequestError(
err: unknown,
request: { path: string; method: string; headers: Record<string, string> },
context: { routerKind: 'Pages Router' | 'App Router'; routePath: string }
) {
const Sentry = await import('@sentry/nextjs');
Sentry.captureException(err, {
tags: {
route: context.routePath,
router: context.routerKind,
method: request.method,
},
});
}
The process.env.NEXT_RUNTIME check matters. Edge and Node bundles are produced separately; importing a Node-only package at the top level of the Edge bundle will fail the build with a cryptic Module not found pointing at a transitive dependency.
Step 4: Verify the cold start fires the hook. Deploy, then trigger a cold start. The easiest way on Vercel is to redeploy without a build (Settings -> Redeploy with same build cache) and immediately hit any server route. In the Functions tab of the deployment, the first log line for the function invocation should be [instrumentation] node init complete. If you see route logs without that line, the hook did not fire and the file is still in the wrong location.
Step 5: Confirm errors are flowing. Add a deliberate throw inside a route handler:
// app/api/_test/route.ts
export async function GET() {
throw new Error('instrumentation-test');
}
Deploy, hit /api/_test, and check Sentry. The event should appear with the route: /api/_test tag from onRequestError. Remove the route once verified.
The Lesson
Next.js 16 made instrumentationHook stable, narrowed its runtime semantics, and started requiring the file to live next to app/ or pages/. Drop the experimental flag, move the file into src/ if that is where your routes live, gate code on NEXT_RUNTIME, and wire onRequestError for route-handler errors.
If your observability went quiet after a Next.js 16 upgrade and no one noticed for a week, that is the kind of cleanup I do. See my services. For a related upgrade issue I covered yesterday, see Next.js 16 Turbopack build OOM on Vercel.