Type something to search...
Next.js instrumentation.js

Next.js instrumentation.js

instrumentation.js is one of the smallest files in the App Router's convention vocabulary and one of the most disproportionately useful once you actually need it. It's the one place in a Next.js app where you can reliably run code exactly once, when a server instance starts, before that instance accepts a single request — plus an optional hook that gives you a structured feed of every server-side error the framework catches, complete with routing context you'd otherwise have to reconstruct by hand.

This is a compact reference: two exported functions, a handful of parameters, and one runtime-targeting trick. What makes it worth reading closely is exactly what each function guarantees and doesn't guarantee, since both of those boundaries matter for building monitoring you can actually trust.

File Location

Place instrumentation.ts (or .js) in the root of your project, or inside your src folder if your project uses one — the same top-level location as next.config.js, not nested inside app. It's a project-level concern, not a route-level one, and its placement reflects that.

register() — Runs Once, Before Anything Else

import { registerOTel } from "@vercel/otel";

export function register() {
  registerOTel("next-app");
}

The contract here is precise and worth internalizing exactly: register is called once, when a new Next.js server instance is initiated, and it must complete before the server is ready to handle requests. register can be async, and Next.js will wait for it.

That "must complete before ready" guarantee is the entire reason this file exists instead of you just importing your tracing SDK setup somewhere convenient in application code. Observability tooling — OpenTelemetry exporters, error-reporting SDKs, feature-flag clients that need an initial fetch — generally needs to be fully initialized before the first real request arrives, or you risk losing exactly the earliest events you'd most want visibility into (cold-start errors, first-request latency spikes). register() gives you that guarantee structurally, rather than hoping your import order happens to work out.

onRequestError() — A Structured Feed of Server Errors

import { type Instrumentation } from "next";

export const onRequestError: Instrumentation.onRequestError = async (
  err,
  request,
  context,
) => {
  const message = err instanceof Error ? err.message : String(err);
  const digest =
    typeof err === "object" && err !== null && "digest" in err
      ? String(err.digest)
      : undefined;

  await fetch("https://.../report-error", {
    method: "POST",
    body: JSON.stringify({ message, digest, request, context }),
    headers: { "Content-Type": "application/json" },
  });
};

Two behavioral details here are easy to get wrong if you skim past them:

If you run async work inside onRequestError, you must await it. The function is triggered when the Next.js server captures an error — it doesn't wait around for orphaned fire-and-forget promises you started and walked away from. If your reporting call isn't awaited, there's no guarantee it completes before the process moves on, and you'll lose error reports intermittently in a way that's maddening to debug because it looks random.

The error instance you receive might not be the original thrown error. If the error was encountered during Server Component rendering, React may have processed it before it reaches you — meaning the object here can differ from what your code actually threw. This is exactly why the digest property exists: use it as your stable correlation key to match this callback's error against the corresponding server-side log entry, rather than relying on message staying consistent.

The Three Parameters in Full

export function onRequestError(
  error: unknown,
  request: {
    path: string; // resource path, e.g. /blog?name=foo
    method: string; // request method, e.g. GET, POST
    headers: { [key: string]: string | string[] };
  },
  context: {
    routerKind: "Pages Router" | "App Router";
    routePath: string; // e.g. /app/blog/[dynamic]
    routeType: "render" | "route" | "action" | "proxy";
    renderSource:
      | "react-server-components"
      | "react-server-components-payload"
      | "server-rendering";
    revalidateReason: "on-demand" | "stale" | undefined;
    renderType: "dynamic" | "dynamic-resume"; // 'dynamic-resume' is PPR
  },
): void | Promise<void>;

The context object is doing a lot of quiet, useful work here. routeType alone tells you whether an error came from rendering a page, handling a Route Handler request, running a Server Action, or executing your Proxy — four fundamentally different code paths that, without this context object, you'd otherwise have to infer from stack traces or guess at. renderSource distinguishes an error during the initial RSC render from one during a subsequent RSC payload streamed to an already-hydrated client — a distinction that matters if you're trying to correlate error rates with, say, a specific navigation pattern rather than initial page loads.

Because error is typed as unknown rather than Error, don't assume its shape — narrow it explicitly (as the example above does with instanceof Error and a manual property check) before reading message or digest off of it. Treating it as a guaranteed Error instance is a common source of runtime crashes inside error-handling code itself, which is about as unhelpful a failure mode as you can create.

Targeting a Specific Runtime

instrumentation.js runs in both the Node.js and Edge runtimes by default, which is convenient until your setup logic genuinely needs to differ between the two — a tracing SDK with a Node-specific transport, for instance, that simply can't run on Edge. process.env.NEXT_RUNTIME is how you branch:

export function register() {
  if (process.env.NEXT_RUNTIME === "edge") {
    return require("./register.edge");
  } else {
    return require("./register.node");
  }
}

export function onRequestError() {
  if (process.env.NEXT_RUNTIME === "edge") {
    return require("./on-request-error.edge");
  } else {
    return require("./on-request-error.node");
  }
}

This pattern — a thin dispatcher in instrumentation.js that requires an environment-specific implementation file — keeps your actual setup logic out of a giant conditional and lets each runtime-specific file import only what that runtime can actually support, which matters because some Node-only packages will fail to even load under the Edge runtime, not just fail to behave correctly.

How This Differs From instrumentation-client.js

It's worth being explicit about a distinction the filename similarity invites confusion over: this file (instrumentation.js) runs server-side only — inside your Next.js server process, whether that's Node.js or Edge. There's a separate, newer convention, instrumentation-client.js, that runs in the browser, before hydration, for client-side monitoring like navigation tracking and frontend performance marks. They solve adjacent but distinct problems, and neither is a substitute for the other — a production observability setup typically wants both, not one or the other.

Version History

VersionChanges
v15.0.0onRequestError introduced; instrumentation became stable
v14.0.4Turbopack support for instrumentation
v13.2.0instrumentation introduced as an experimental feature

Key Takeaways

ExportGuarantee
register()Runs exactly once per server instance, must complete before requests are served, can be async
onRequestError()Fires on every captured server-side error; always await internal async work
error paramTyped unknown; may not be the original thrown error — use digest to correlate with server logs
context paramTells you which router, which route, and which rendering phase the error came from
Runtime targetingUse process.env.NEXT_RUNTIME to branch Node.js vs. Edge-specific setup code
ScopeServer-side only — pair with instrumentation-client.js for browser-side observability

For a two-function file, instrumentation.js carries a lot of responsibility: it's the difference between observability tooling that's reliably wired up from the very first request, versus tooling initialized somewhere in application code where cold-start behavior is left to chance. Get register and onRequestError right once, and the rest of your monitoring stack builds on a foundation that actually holds up under real production traffic.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper