Next.js shipped instrumentation.ts in version 13.4 and stabilized it in 15. It is the official, supported way to attach monitoring code to your Next.js server. If you're shipping Next.js to production and not using it, you're either missing telemetry or paying for a more complicated setup than you need.
This guide explains exactly what instrumentation.ts is, what you can intercept with it, how to build a basic monitoring hook manually, and when it's worth using a dedicated tool instead.
What instrumentation.ts Is
instrumentation.ts is a special file that lives at the root of your Next.js project (next to next.config.js) or inside src/ if you use that layout. Next.js looks for it on server boot and calls its exported register function once, before any requests are handled.
// instrumentation.ts
export async function register() {
// This runs once, on the server, at boot time
}That's the entire contract. Whatever you put in register() runs in the server runtime before traffic starts hitting your routes. It is the right place to:
- Initialize monitoring SDKs (Sentry, Datadog, OpenTelemetry, Nurbak Watch)
- Register OpenTelemetry instrumentations
- Set up structured loggers
- Wire up custom metrics collectors
- Validate environment configuration before serving traffic
How to Enable It
In Next.js 15, instrumentation.ts is enabled by default. In earlier versions you needed to flip an experimental flag. Just create the file at the project root:
// instrumentation.ts (project root, sibling of next.config.js)
export async function register() {
console.log('Server booted at', new Date().toISOString())
}Deploy and check your server logs. You should see the message exactly once per cold start of each function instance.
What You Can Intercept
The register function runs once. To actually capture per-request data, you need to attach hooks during register that listen to the runtime. The patterns differ depending on what you want to instrument.
Pattern 1: OpenTelemetry Auto-Instrumentation
The most common pattern. Register OpenTelemetry SDKs and they will auto-instrument http, fetch, your database driver, and more.
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { registerOTel } = await import('@vercel/otel')
registerOTel({ serviceName: 'my-nextjs-app' })
}
}The dynamic import is intentional — it ensures Node-only modules don't get bundled into the Edge runtime, which would break the build.
Pattern 2: Error Tracking with onRequestError
Next.js 15 added an onRequestError export to instrumentation.ts. This hook fires whenever a server-side error is thrown from a route handler, server action, server component, or middleware.
// instrumentation.ts
export async function register() {
// SDK init...
}
export async function onRequestError(
err: Error,
request: { path: string; method: string; headers: Record<string, string> },
context: { routerKind: 'Pages Router' | 'App Router'; routePath: string; routeType: 'render' | 'route' | 'action' }
) {
console.error('Server error:', {
path: request.path,
method: request.method,
route: context.routePath,
message: err.message,
stack: err.stack,
})
// Forward to your error tracking backend
}This gives you a single chokepoint for every server-side error in your application. No more wrapping every route handler in try/catch.
Pattern 3: Manual Request Timing via Middleware
instrumentation.ts doesn't directly hook into the request lifecycle for timing. For that you need middleware.ts at the project root, paired with header propagation:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const start = Date.now()
const response = NextResponse.next()
response.headers.set('x-request-start', start.toString())
return response
}
export const config = {
matcher: '/api/:path*',
}Then in your route handlers, you can read the header to calculate elapsed time. This works but is fragile — you have to do it in every route or use a wrapper. A monitoring SDK does this for you.
Building a Basic Monitoring Hook Manually
Here's a minimal monitoring setup that uses instrumentation.ts + onRequestError to log every error and ship it to a webhook. Useful as a learning exercise or when your needs are very simple.
// instrumentation.ts
export async function register() {
console.log('Monitoring initialized')
}
export async function onRequestError(
err: Error,
request: { path: string; method: string },
context: { routePath: string; routeType: string }
) {
const payload = {
timestamp: new Date().toISOString(),
message: err.message,
stack: err.stack,
route: context.routePath,
method: request.method,
path: request.path,
env: process.env.NODE_ENV,
}
// Best-effort fire-and-forget — don't block the response
fetch(process.env.MONITORING_WEBHOOK_URL!, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(payload),
}).catch(() => {
// Swallow — never let monitoring break your app
})
}This is roughly 20 lines of code and gives you basic error reporting. It's not fancy — no grouping, no rate limiting, no dashboards, no alerting — but it's functional.
Why a Dedicated Tool Saves Time
The minimal example above works for "errors land in a webhook". The moment you want any of the following, the line count balloons:
- Error grouping (collapsing identical errors into a single issue)
- Rate limiting (don't ship 10K events when one route loops)
- Dashboards (UI to see what's happening)
- P50/P95/P99 latency calculation from raw request data
- Alerting (Slack, email, WhatsApp routing with thresholds)
- External uptime checks (pings from outside your infra)
- Multi-region monitoring
- Source map support for readable stack traces
Each of these is a few days to a week of engineering work. Most teams hit the break-even point with a dedicated tool in well under a sprint.
Tools That Use instrumentation.ts
- Sentry: Hooks into
onRequestErrorfor error tracking. Mature, best in class for stack traces. - OpenTelemetry / @vercel/otel: Standards-based tracing. Verbose to set up but works with any OTel backend.
- Nurbak Watch: Purpose-built for Next.js. Single SDK that handles uptime + latency + error rates without an agent. 5 lines of code in
instrumentation.ts. - Datadog: Has a Vercel integration that uses instrumentation.ts under the hood, plus an agent for self-hosted Next.js.
Code Example with Nurbak Watch
// instrumentation.ts
import { initWatch } from '@nurbak/watch'
export async function register() {
initWatch({
apiKey: process.env.NURBAK_WATCH_KEY,
// Optional config:
sampleRate: 1.0, // Track 100% of requests
ignoreRoutes: ['/api/health'],
})
}
export const onRequestError = async (err, request, context) => {
const { reportError } = await import('@nurbak/watch')
reportError(err, { request, context })
}This handles uptime monitoring, internal latency tracking, error rates, and alerting. Auto-discovers every API route in your app. Reports to a hosted dashboard with multi-region pings included.
Common Mistakes
- Importing Node modules at the top of instrumentation.ts. Use dynamic
await import()guarded byprocess.env.NEXT_RUNTIME === 'nodejs'to avoid breaking the Edge build. - Doing async work that blocks register(). register() runs on every cold start. Long-running setup makes cold starts slower.
- Letting monitoring throw. Wrap monitoring SDK init in try/catch. A broken monitoring SDK should never bring down your app.
- Forgetting onRequestError. The register function runs once. onRequestError runs per error. Both are needed for complete coverage.
- Shipping PII in error payloads. Strip cookies, auth headers, and request bodies before forwarding errors externally.
The Bottom Line
instrumentation.ts is the right answer for Next.js monitoring. It replaces traditional APM agents, works in serverless, and gives you a single place to wire up everything from logging to error tracking to uptime monitoring.
If your needs are simple, build it manually in 20-50 lines. If you need anything beyond basic error reporting, use a dedicated tool — you'll save engineering time and get better signal.
Try Nurbak Watch:Start free — 5 lines in instrumentation.ts, 3 endpoints, no credit card.

