Implementing OpenTelemetry in Next.js 15
Learn how to implement OpenTelemetry in Next.js 15 using @vercel/otel and instrumentation.ts. This guide covers production tuning like setting a 0.1 trace ratio and compares observability backends such as OpenObserve, Jaeger, and Grafana Tempo.
Automated instrumentation in Next.js 15
Next.js 15 detects instrumentation.ts automatically. I skip the experimental: { instrumentationHook: true } flag because it is ignored in this version. You should place this file in your project root or inside the src folder, if you are using one. You already know how the directory structure works.
Use @vercel/otel.
For most applications, use @vercel/otel because it handles both Node.js and Edge runtimes from a single file. If you prefer the manual NodeSDK route, you must guard your imports with if (process.env.NEXT_RUNTIME === 'nodejs') to prevent build errors when the runtime attempts to bundle Node-only modules for the edge. I find the manual setup far too verbose for most projects. You can install the required dependencies with:
yarn add @opentelemetry/api @opentelemetry/api-logs @opentelemetry/sdk-node @opentelemetry/instrumentation-http @opentelemetry/instrumentation-fetch @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-logs-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc @opentelemetry/resources @opentelemetry/semantic-conventions
Next.js instruments several parts of your app automatically. It captures traces for API routes, the App router, the Page router, fetch calls, Server Actions, and Middleware. It also traces database queries if you use an instrumented client like Prisma or pg. The framework emits top-level spans like BaseServer.handleRequest for incoming requests and AppRender.fetch for code-executed fetch requests. You can also see spans for AppRouteRouteHandlers.runHandler during API route execution. If you need more granularity, set NEXT_OTEL_VERBOSE=1 in your environment. This setting emits extra traces for code execution.
Managing production telemetry
Production environments require specific tuning. I set a 0.1 ratio for the TraceIdRatioBasedSampler in production to capture 10% of traces. This keeps costs low. Use a ParentBasedSampler to ensure child spans follow the root decision. For span processing, use BatchSpanProcessor rather than SimpleSpanProcessor. It buffers spans and sends them in groups every 5 seconds or when the batch reaches 512 items. This reduces the latency impact on your handlers. I avoid SimpleSpanProcessor because it processes spans as they are created, which creates significant overhead.
I skip the flags.
Next.js 15 introduced onRequestError in the instrumentation.ts file. This hook fires for unhandled errors in server components, route handlers, and server actions. You can use it to intercept all server errors and attach them to the current active request span. This provides useful visibility into failures. I find that attaching errors to the active span allows for easier troubleshooting in your dashboard.
In development, you can set your sampler to 1.0 to capture every trace. I recommend this to ensure you see every interaction. For production, a 0.1 ratio is a solid baseline. If you use the NodeSDK directly, you can also configure the sampler inline.
Selecting an observability backend
Choosing a backend determines how you find errors. OpenObserve provides a unified platform for traces, logs, and metrics. It uses columnar storage to handle high-cardinality data. I find it cheaper than Elasticsearch-based stacks. OpenObserve accepts OTLP traces directly without requiring proprietary agents. Jaeger provides a standalone, CNCF-graduated option for Kubernetes teams. It is leaner than Zipkin because it was written in Go. Jaeger supports adaptive sampling to adjust rates based on traffic.
Grafana Tempo uses object storage like S3 to keep costs low. It uses the TraceQL query language to search for attributes, duration, and status. You can find traces through Loki logs or Grafana metrics. Zipkin runs as a single binary that is easy to start. I skip Zipkin for production because it lacks adaptive sampling and multi-tenancy.
Datadog is a full APM suite. It connects traces to logs and metrics in one interface. However, the pricing is unpredictable. A team with 20 hosts and moderate traffic can expect to pay $1,000 to $3,000 per month. Honeycomb handles high-cardinality data well. It uses a columnar store to treat spans as events. Its BubbleUp feature identifies which attributes differ between slow and fast requests.
| Tool | Type | Best Use Case |
|---|---|---|
| OpenObserve | Unified | Low-cost unified signals |
| Jaeger | Open Source | Kubernetes-native teams |
| Grafana Tempo | Object Storage | Grafana stack users |
| Zipkin | Open Source | Small, simple projects |
Does the manual setup provide enough control for your custom exporters?