How to Set Up OpenTelemetry Tracing in a Next.js App
Most engineering teams add observability after something breaks in production. That is the wrong order. By the time a user-facing bug surfaces, the trace that would have explained it is already gone. Instrumenting your Next.js application with OpenTelemetry from the start means you have the full picture — latency, errors, and request flows — before anyone files a support ticket.
This guide walks through a complete OpenTelemetry setup for a Next.js app, from installing the SDK to exporting spans to a self-hosted Jaeger or Grafana Tempo backend.
What OpenTelemetry Actually Does
OpenTelemetry (OTel) is a vendor-neutral observability framework maintained by the CNCF. It provides a unified API and SDK for capturing traces, metrics, and logs from your application. A trace is a structured record of a single request as it moves through your system — across API routes, database queries, external HTTP calls, and server components.
Next.js is a full-stack framework, so a single user action can touch the browser, a Next.js API route, a database, and a third-party service. Without tracing, debugging that chain means reading scattered logs and guessing. With tracing, you see the entire waterfall in one view.
Prerequisites
- Next.js 13+ (App Router or Pages Router both work)
- Node.js 18+
- Docker (to run Jaeger or Grafana Tempo locally)
- Basic familiarity with environment variables and
next.config.js
Step 1 — Install the OpenTelemetry Packages
Next.js has built-in experimental support for OpenTelemetry instrumentation via its @vercel/otel package, but for full control — especially when self-hosting — use the core OTel SDK directly.
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/resources \
@opentelemetry/semantic-conventions
These packages handle:
sdk-node— the Node.js OTel SDK runtimeauto-instrumentations-node— automatic instrumentation for HTTP, DNS, Express, fetch, and moreexporter-trace-otlp-http— ships spans to any OTLP-compatible backend over HTTP
Step 2 — Create the Instrumentation File
Next.js 13.4+ supports a special instrumentation.ts file at the project root. This file runs once when the server starts — before any route is handled — making it the correct place to initialize OTel.
Create instrumentation.ts:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SEMRESATTRS_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
export function register() {
const exporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? 'http://localhost:4318/v1/traces',
});
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: process.env.OTEL_SERVICE_NAME ?? 'nextjs-app',
}),
traceExporter: exporter,
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // too noisy
}),
],
});
sdk.start();
}
Then enable it in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true,
},
};
module.exports = nextConfig;
The instrumentationHook flag tells Next.js to load your instrumentation.ts file on server start.
Step 3 — Run a Local Jaeger Backend
Jaeger is the easiest backend for local development. It accepts OTLP over HTTP on port 4318 and provides a built-in UI on port 16686.
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
Open http://localhost:16686 in your browser. Once your Next.js app is running and handling requests, traces will appear in the Jaeger UI under the service name you set in OTEL_SERVICE_NAME.
Step 4 — Switching to Grafana Tempo in Production
Jaeger is convenient locally but Grafana Tempo is better suited for production. Tempo stores traces cheaply in object storage (S3 or GCS) and integrates natively with Grafana dashboards.
To point your exporter at a Tempo instance, change only the environment variable:
OTEL_EXPORTER_OTLP_ENDPOINT=http://your-tempo-host:4318/v1/traces
No code changes required. That is the value of the OTLP standard — your instrumentation is decoupled from your backend choice.
In Grafana, add a Tempo data source, then use Explore → TraceQL to query traces. You can also correlate traces with Prometheus metrics using exemplars, which gives you a direct link from a latency spike on a dashboard to the specific slow trace that caused it.
Step 5 — Adding Custom Spans
Auto-instrumentation captures HTTP requests and outbound calls automatically. For business logic — like a payment processing function or a complex data transformation — add manual spans:
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('payment-service');
export async function processPayment(orderId: string) {
return tracer.startActiveSpan('processPayment', async (span) => {
span.setAttribute('order.id', orderId);
try {
const result = await chargeCard(orderId);
span.setAttribute('payment.status', result.status);
return result;
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: 2, message: (err as Error).message });
throw err;
} finally {
span.end();
}
});
}
Custom spans give you fine-grained visibility into your application's critical paths, not just its infrastructure layer.
Common Pitfalls to Avoid
- Initializing OTel inside a route handler. The SDK must start once, not per request. Always use
instrumentation.ts. - Forgetting to call
span.end(). Spans that never close will either cause memory leaks or never be exported. Usetry/finallyreligiously. - Ignoring sampling in production. At high traffic volumes, exporting every trace is expensive. Configure a tail-based or probability sampler to capture 10–20% of traces, but always 100% of errored ones.
- Treating trace IDs as a debug-only tool. Log the active trace ID alongside your application logs so you can correlate a log line directly to its trace. The OTel API provides
trace.getActiveSpan()?.spanContext().traceIdfor exactly this.
Why This Matters for Your Project
Observability is not a feature you bolt on before going to production — it's the difference between guessing why your API is slow and knowing exactly which database call or third-party dependency is responsible. For SaaS teams shipping on Next.js, OpenTelemetry gives you a vendor-neutral foundation that works whether you are running on Vercel, a Kubernetes cluster, or a single VPS. Instrument early, and every incident becomes a learning event rather than a firefighting exercise.




