OpenTelemetry Next.js: Tracing for App Router and Pages Router
Next.js applications run code in multiple environments — server, edge runtime, and browser — which makes standard monitoring approaches incomplete. This guide covers OpenTelemetry instrumentation for Next.js 15 and 16, including App Router, Pages Router, and production configuration.
Vercel's built-in observability covers per-function timing but not full request traces across middleware, database queries, and third-party API calls. OpenTelemetry sends traces to any OTLP-compatible backend so you're not tied to Vercel's platform.
Quick start: For zero-code setup, see the Node.js zero-code instrumentation guide.
Next.js version compatibility
| Next.js | instrumentation.ts | instrumentationHook flag |
|---|---|---|
| 13.4–14.x | Experimental | Required in next.config |
| 15.x | Stable | Removed — delete it |
| 16.x | Stable | Removed — delete it |
Starting with Next.js 15, instrumentation.ts is detected automatically. If your config still contains experimental: { instrumentationHook: true }, remove it — it is ignored and clutters your config.
Setup with @vercel/otel (recommended)
@vercel/otel is an Edge-compatible wrapper around the OpenTelemetry SDK. It initializes in both Node.js and Edge runtimes from a single file, which is the main reason to prefer it over the raw NodeSDK for most Next.js apps.
Install:
npm install @vercel/otel @opentelemetry/api @opentelemetry/api-logs \
@opentelemetry/sdk-logs @opentelemetry/instrumentation
Create instrumentation.ts at the project root (not inside app/ or pages/):
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({ serviceName: 'nextjs-app' })
}
Set environment variables:
Note: The examples below use Uptrace as the OTLP backend. Replace the endpoint and headers with Jaeger, Grafana Tempo, or your own collector.
# .env.local
OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev
OTEL_EXPORTER_OTLP_HEADERS=uptrace-dsn=<your_uptrace_dsn>
OTEL_SERVICE_NAME=nextjs-app
Replace the endpoint with any OTLP-compatible backend — Jaeger (http://localhost:4318), Grafana Tempo, or your own collector.
That's the entire setup for Next.js 15 and 16. No next.config.ts changes needed.
Manual setup with NodeSDK
Use the NodeSDK directly when you need a custom sampler, specific span processors, or exporters that @vercel/otel doesn't expose. The downside is that NodeSDK doesn't work in Edge runtime — you must guard the import.
Install:
npm install @opentelemetry/sdk-node @opentelemetry/api \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources @opentelemetry/semantic-conventions
instrumentation.ts:
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node')
}
// Edge: use @vercel/otel if edge runtime support is needed
}
instrumentation.node.ts:
import { NodeSDK } from '@opentelemetry/sdk-node'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { Resource } from '@opentelemetry/resources'
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: 'nextjs-app',
}),
spanProcessor: new BatchSpanProcessor(
new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
})
),
})
sdk.start()
Edge runtime limitations
NodeSDK does not run in the Edge runtime. If your app uses Middleware or Edge Functions alongside regular Node.js routes, use @vercel/otel for the edge parts. The conditional import pattern above (NEXT_RUNTIME === 'nodejs') prevents Module not found build errors when Node.js-only modules are bundled.
What Next.js traces automatically
With either setup initialized, Next.js automatically creates spans for:
- Incoming HTTP requests (App Router and Pages Router)
fetchcalls from server components and route handlers- Server Actions
- Middleware execution
- Database queries via any instrumented client (pg, prisma, etc.)
To see more verbose spans, set NEXT_OTEL_VERBOSE=1.
App Router instrumentation
Server components, route handlers, and server actions are all traced automatically once the SDK is initialized. You don't need to add any code to existing components.
Route handler (automatic):
// app/api/users/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const users = await db.query('SELECT * FROM users LIMIT 10')
return NextResponse.json(users)
}
Custom span in a server component:
// app/users/page.tsx
import { trace } from '@opentelemetry/api'
const tracer = trace.getTracer('nextjs-app')
async function getUsers() {
return tracer.startActiveSpan('db.getUsers', async (span) => {
try {
const users = await db.query('SELECT * FROM users')
span.setAttribute('users.count', users.length)
return users
} finally {
span.end()
}
})
}
export default async function UsersPage() {
const users = await getUsers()
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
Pages Router instrumentation
API routes in pages/api and getServerSideProps are traced automatically. getStaticProps runs at build time — traces from static generation are not sent to your backend in standard configuration because there's no running server during next build.
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const users = await db.query('SELECT * FROM users')
res.status(200).json(users)
}
Capturing server errors
Next.js 15 added onRequestError to instrumentation.ts. It fires for unhandled errors in server components, route handlers, and server actions, and is the correct place to attach errors to traces:
// instrumentation.ts
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({ serviceName: 'nextjs-app' })
}
export async function onRequestError(
err: Error,
request: { path: string; method: string },
context: { routerKind: string; routePath: string }
) {
const { trace } = await import('@opentelemetry/api')
const span = trace.getActiveSpan()
if (span) {
span.recordException(err)
span.setStatus({ code: 2, message: err.message })
}
}
Production configuration
At full request volume, sampling 100% of traces is expensive. Set a ratio-based sampler via environment variables — no code changes needed:
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
OTEL_LOG_LEVEL=info
parentbased_traceidratio at 0.1 samples 10% of new root traces while respecting the sampling decision from upstream services. Adjust the ratio based on your request volume and cost tolerance.
For NodeSDK you can also configure it inline:
import { TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-base'
const sdk = new NodeSDK({
sampler: new TraceIdRatioBasedSampler(
process.env.NODE_ENV === 'production' ? 0.1 : 1.0
),
})
Deployment
Vercel
Set environment variables in Project Settings → Environment Variables. Next.js 15 and 16 detect instrumentation.ts automatically — no additional Vercel configuration needed.
Docker
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
Pass environment variables at runtime:
docker run -p 3000:3000 \
-e OTEL_EXPORTER_OTLP_ENDPOINT=https://api.uptrace.dev \
-e OTEL_EXPORTER_OTLP_HEADERS="uptrace-dsn=<your_uptrace_dsn>" \
-e OTEL_SERVICE_NAME=nextjs-app \
nextjs-app
For collector setup patterns, see the OpenTelemetry Docker guide.
Troubleshooting
No traces appearing
Verify instrumentation.ts is at the project root, not inside app/ or pages/. Confirm the register function is exported. On Next.js 14 and earlier, add experimental: { instrumentationHook: true } to next.config.ts.
Edge runtime build errors
You're importing Node.js-only modules in a file that gets bundled for edge. Either switch to @vercel/otel, or guard the import with if (process.env.NEXT_RUNTIME === 'nodejs').
Duplicate spans
Don't mix automatic and manual instrumentation for the same operation. Let the SDK handle framework-level spans; add startActiveSpan only for custom business logic.
Variables not found in browser
Server-side environment variables (without NEXT_PUBLIC_ prefix) are only available on the server. OTel configuration should stay server-side — don't expose DSNs or endpoints to the client.
What is Uptrace?
Uptrace is an open source APM for OpenTelemetry that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.
Uptrace comes with an intuitive query builder, rich dashboards, alerting rules, notifications, and integrations for most languages and frameworks. It can process billions of spans on a single server at a fraction of the cost of hosted alternatives.
Try it via the cloud demo (no login required) or run it locally with Docker. Source code on GitHub.
FAQ
- Does Next.js 15 need the instrumentationHook flag? No. The
instrumentationHookexperimental flag was removed in Next.js 15.instrumentation.tsis detected automatically. If you haveexperimental: { instrumentationHook: true }from an earlier version, remove it. - Does OpenTelemetry work with App Router and server components? Yes. Both
@vercel/oteland the manual NodeSDK instrument App Router server components, route handlers, and server actions automatically. Custom spans withtrace.getTracer()work in any server-side context. - Can I use OpenTelemetry in the Edge runtime? The manual NodeSDK does not run in the Edge runtime. Use
@vercel/otel— it handles both Node.js and Edge runtimes from a singleinstrumentation.tswithout extra configuration. - What does Next.js trace automatically? With the SDK initialized, Next.js traces incoming HTTP requests,
fetchcalls from server-side code, server actions, middleware, and queries from instrumented database clients. SetNEXT_OTEL_VERBOSE=1for more detailed spans.
What's next
- OpenTelemetry JavaScript guide — full JS SDK documentation
- OpenTelemetry distributed tracing — concepts and context propagation
- OpenTelemetry Node.js instrumentation — additional library instrumentation