1. Blog
  2. Next.js Monitoring: Find Slow Routes and Server Errors in the App Router
Blog

Next.js Monitoring: Find Slow Routes and Server Errors in the App Router

A slow Next.js page is usually a slow route on the server, and a production error hides behind a digest. Here's how to find both in the App Router.

The Atatus TeamPublished October 12, 202611 min read
Next.js Monitoring: Find Slow Routes and Server Errors in the App Router

Two things make Next.js App Router apps hard to debug in production. A slow page is usually slow on the server, before the browser receives anything, so browser tools only show a long wait. And when a Server Component throws in a production build, Next.js replaces the error message with a generic one and a digest, so the real error never reaches the browser.

This guide covers the common reasons App Router routes get slow, how to find the slow route and the call inside it, how to capture the server errors Next.js hides, and what to watch in the browser. The examples use Next.js 16 and the Atatus Node.js agent, but the causes and fixes apply whatever tool you monitor with.

Table of Contents

Why Next.js apps are hard to monitor in production

  • Server time is invisible to the browser. On a route rendered at request time, Server Components fetch their data and render on the server for each request. In Next.js 16, fetch requests are not cached by default, and an uncached request blocks the page from rendering until it finishes. The user sees a blank wait, and the browser only records a long time to first byte.
  • Production errors hide their message. When a Server Component throws in a production build, the browser gets a generic error and a digest. On Next.js 16.3 and later it reads "Minified React error #441", which links to React's full text. Earlier versions show that text directly: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details." The message and stack trace stay on the server.
  • Many URLs run the same route. /products/1 and /products/2 run the same code. Monitoring that groups requests by URL splits one slow route into thousands of rows, each too small to notice. You need requests grouped by route, such as /products/[id].
  • The work runs in several places. One page view can involve proxy.ts (called middleware before Next.js 16), Server Component rendering, Route Handlers, Server Actions and Client Components in the browser. Any of them can be the slow or failing part.

Six common reasons an App Router route is slow

1. Requests that wait for each other

Inside one component, each await waits for the previous one to finish. Two independent requests written one after the other take the sum of both durations:

// app/artist/[username]/page.tsx
export default async function Page({ params }: { params: Promise<{ username: string }> }) {
  const { username } = await params
  const artist = await getArtist(username)   // waits
  const albums = await getAlbums(username)   // starts only after getArtist finishes
  return <ArtistPage artist={artist} albums={albums} />
}

If the second request doesn't need the first one's result, start both and wait for them together, so the route takes as long as the slower one:

const [artist, albums] = await Promise.all([getArtist(username), getAlbums(username)])

If one request really does depend on the other, render the dependent part in its own component inside <Suspense>, so the rest of the page can stream to the browser while it loads.

2. Uncached data on every request

Since fetch isn't cached by default and database queries always run, a route rendered at request time repeats every data call on every request. For data that rarely changes, such as categories, settings or navigation, caching removes that work. In Next.js 16, turn on Cache Components and add the "use cache" directive to the function, component or page you want cached:

// next.config.ts (partialPrefetching needs Next.js 16.3+; since 16.4, leaving it unset logs a warning)
const nextConfig = { cacheComponents: true, partialPrefetching: true }
export default nextConfig

// app/lib/data.ts
import { db, categories } from '@/lib/db'

export async function getCategories() {
  'use cache'
  return db.select().from(categories)
}

This changes the whole app. After you turn it on, routes that read uncached or request data outside <Suspense> fail validation, and segments that export dynamic, revalidate or fetchCache error, so follow the migration guide on an existing app.

For data that must be fresh on every request, don't cache it. Move it into a component wrapped in <Suspense> instead, so it no longer holds up the rest of the page.

3. Request data read in a layout

A loading.tsx file shows a loading state while a page renders, but it doesn't cover the layout in the same folder. When a layout reads cookies() or headers(), or makes an uncached fetch, the loading state never shows and navigation waits until the layout finishes. With Cache Components on, Next.js reports this as a build-time error instead. A user menu that reads a session cookie in the root layout is a typical example.

Move that read into a small component wrapped in its own <Suspense>, or move it into the page where loading.tsx covers it:

// app/layout.tsx
import { Suspense } from 'react'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <Suspense fallback={<UserMenuSkeleton />}>
            <UserMenu /> {/* reads cookies() */}
          </Suspense>
        </header>
        {children}
      </body>
    </html>
  )
}

4. The same query run several times per request

Next.js memoizes identical fetch requests within one render, so a layout and a page can fetch the same URL and only one request goes out. Database and ORM calls get no such help. If the layout, the page and generateMetadata each load the same product, the database gets the same query three times. Wrap the function in React's cache so every caller in the same request shares one result:

// app/lib/products.ts
import { cache } from 'react'
import { eq } from 'drizzle-orm'
import { db, products } from '@/lib/db'

export const getProduct = cache(async (id: string) => {
  return db.query.products.findFirst({ where: eq(products.id, id) })
})

5. One slow query or API call

Often the route's code is fine and one thing it calls is slow: a query that scans a table because an index is missing, an ORM call that loads far more rows than the page shows, or a third-party API that takes seconds to answer. Reading the code won't tell you which one it is. You need timing for each database query and external call inside the request. See diagnosing slow database queries in Node.js and reading PostgreSQL EXPLAIN plans for the next step once you've found the query.

6. Too much work in the browser

Some pages arrive quickly and still feel slow: large Client Components, heavy JavaScript bundles and slow hydration delay what the user can see and click. Server monitoring can't show this, because the server finished its work long before. It shows up in browser metrics such as Largest Contentful Paint (LCP) and Interaction to Next Paint (INP), covered below.

How to find the slow route and the slow call

Whatever tool you use, the search goes from the route, to its breakdown, to one slow request. In Atatus it looks like this.

  1. Group requests by route. On Next.js 16, Atatus Node.js agent 3.5.3 and later names App Router pages and Route Handlers by the route they matched, such as GET /products/[id]. Earlier agent versions, and older Next.js versions, group them under names like GET unknown route.
  2. Look at time and traffic together. The Transactions page shows each route's response time and throughput, with lists of the slowest, highest-volume and most-failed transactions. A route with a modest average and heavy traffic can cost your users more time than a rarely visited slow one.
  3. Break the route's time down. Open the transaction. The Performance Breakdown tab shows how its time splits across layers such as database calls and external requests, so you know whether to look at queries, APIs or your own code.
  4. Open one slow request. The Session Traces tab lists individual traced requests with their duration. Each trace shows database calls and external requests in order, with their durations, and the database icon (Show Query) reveals the SQL. By default the agent traces only requests of 2 seconds or more, and keeps up to 5 per send interval, so set ATATUS_TRACE_THRESHOLD (in milliseconds) lower if your slow routes are faster than that.

The shape of the trace usually points to the cause. Calls that start one after another are a waterfall (cause 1). The same query several times in one request is cause 4. One long bar in an otherwise short trace is cause 5.

How to fix "An error occurred in the Server Components render"

This message, or Minified React error #441 on Next.js 16.3 and later, isn't the error itself. It's what Next.js shows in place of any error thrown while rendering on the server in a production build, so you can't fix it until you see the original error and its stack trace. The first step is to capture it.

Next.js catches errors thrown while rendering Server Components, in Route Handlers and in Server Actions, then serves its own error response. Because the framework handles the error before it reaches Node.js, a monitoring agent's automatic error capture never sees it.

Next.js 15 added a hook for exactly this. Export an onRequestError function from instrumentation.ts (or .js) and Next.js calls it whenever its server captures an error. It receives the error, the request (path, method and headers) and the context, which includes the route file path, such as /app/blog/[slug], and where the error happened: render, route, action or proxy.

To send these errors to Atatus, create instrumentation.js in the project root, or inside src/ if you use one, never inside app/. This assumes the agent is already loaded and listed in serverExternalPackages (steps 2 and 4 below). Otherwise the errors are dropped silently:

// instrumentation.js
export async function onRequestError(err, request, context) {
  if (process.env.NEXT_RUNTIME !== "nodejs") {
    return;
  }

  const atatus = require("atatus-nodejs");

  atatus.notifyError(
    err instanceof Error ? err : new Error(String(err)),
    {
      handled: false,
      customData: {
        source: "next-on-request-error",
        // Path and method only: request.headers carries cookies and auth tokens
        request: { path: request.path, method: request.method },
        context,
        digest:
          err && typeof err === "object" && "digest" in err
            ? String(err.digest)
            : undefined,
      },
    }
  );
}

Run next build and restart the server after adding the file. The errors then appear under Exceptions with their full stack trace, and the digest, the request path and method, and the route context are saved with each one. When a user reports an error page with a digest, you can match it to the server error that caused it. The TypeScript version and troubleshooting steps are in the Atatus docs.

Once you can read the original error, the cause is usually ordinary: an environment variable set locally but missing in production, a database or API call that failed or timed out, or code that assumed a record exists when it doesn't. Because it only fails in production, the recorded request and route are often what you need to reproduce it.

The hook only covers the server. Errors in Client Components, including errors in event handlers that error boundaries don't catch, need the browser agent described below.

Setting up Next.js monitoring with Atatus

  1. Install the agent. Use version 3.5.4 or later. Version 3.5.3 added route names for Next.js 16 and instrumentation of the packages Next.js links into .next/node_modules when you build, and 3.5.4 fixed a syntax error in 3.5.3's mongodb-core instrumentation. That folder is scanned once when the server starts, so it covers next start, not next dev.
    npm install --save atatus-nodejs
  2. Start Next.js with the agent loaded. Add NODE_OPTIONS to your dev and start scripts, or set it in whatever process manager runs your server:
    "scripts": {
      "dev": "NODE_OPTIONS='-r atatus-nodejs/start-next.js' next dev",
      "build": "next build",
      "start": "NODE_OPTIONS='-r atatus-nodejs/start-next.js' next start"
    }
  3. Set your license key and app name.
    export ATATUS_LICENSE_KEY="YOUR_LICENSE_KEY"
    export ATATUS_APP_NAME="YOUR_APP_NAME"
  4. Keep the agent and your database drivers out of the bundle. Next.js bundles server dependencies by default. The agent instruments a package when Node.js loads it with require(), so a driver compiled into the bundle goes unseen and its queries are missing from your traces. Next.js already keeps some drivers, such as pg and mongodb, out of the bundle. List the agent and any other drivers you use in serverExternalPackages:
    // next.config.js
    const nextConfig = {
      serverExternalPackages: ["atatus-nodejs", "mysql2", "ioredis"],
    };
    
    module.exports = nextConfig;
  5. Add onRequestError as shown in the previous section.
  6. Add the browser agent to your root layout with next/script. The Next.js RUM setup guide has the snippet.

Run next build, restart the server and send a few requests. Data appears in the dashboard within minutes. The Next.js APM setup guide lists the optional settings.

What to watch in the browser

After the first page load, App Router navigations happen in the browser: Next.js loads the new route's payload, from the server or its client cache, and swaps the page without a full reload. Server monitoring sees those requests, but not what the user sees and waits for. That needs real user monitoring.

  • Route changes, not only page loads. Atatus RUM records client-side route changes alongside full page loads, with how long each took and the Ajax calls it made. A route can be fast on a full load and slow on navigation, or the other way round.
  • Web Vitals. LCP shows when the main content appeared, INP how quickly the page responded to clicks and typing, and CLS how much the layout shifted, each split into good, needs improvement and poor, for desktop and mobile. Slow hydration and heavy Client Components show up here first.
  • JavaScript errors. Uncaught errors in Client Components, including errors in event handlers, reach JS Errors with the browser and page they happened on. Render errors caught by an error.tsx don't, so report them yourself by calling window.atatus?.notify(error) in that boundary's useEffect. Set productionBrowserSourceMaps: true so next build emits source maps, then upload them so stack traces point to your source instead of minified bundles. See debugging minified JavaScript with source maps and JavaScript error monitoring best practices.

A Next.js monitoring checklist

  • Server requests are grouped by route, such as GET /products/[id], not by URL.
  • Response time and throughput are tracked for each route, and you check the slowest and busiest ones regularly.
  • The trace threshold is low enough to catch your slow routes, and their traces show database queries and external calls, so you can spot waterfalls, repeated queries and slow dependencies.
  • onRequestError reports server errors with their stack trace and digest.
  • The agent and your database drivers are listed in serverExternalPackages.
  • The browser agent records route changes, Web Vitals and JavaScript errors.
  • Source maps are generated and uploaded for every release.

Frequently asked questions

How do I find the real error behind "An error occurred in the Server Components render"?

Next.js removes the message from production builds so it can't leak sensitive details, and keeps the original error on the server. Export onRequestError from instrumentation.ts and send the error, with its digest, to your monitoring tool. The digest the user saw then matches the stack trace you recorded.

Why is client-side navigation slow in the Next.js App Router?

When a route is rendered at request time, navigating to it still needs the server to render it, so a slow server render makes the navigation slow too. Find out which part of the render is slow with the steps above, and add a loading.tsx to the route so Next.js shows a loading state straight away while the page renders.

Does Atatus support the Next.js 16 App Router?

Yes. Node.js agent 3.5.3, released in July 2026, names App Router pages and Route Handlers by their matched route and instruments the packages Next.js 16 links into .next/node_modules. Use 3.5.4 or later, which fixed a syntax error in 3.5.3.

Why are database queries missing from my Next.js traces?

Next.js bundles server dependencies by default, and the agent can only instrument a package that Node.js loads at runtime. Add the agent and any database driver that Next.js doesn't already keep external, such as mysql2 or ioredis, to serverExternalPackages in next.config.js, then rebuild and restart. Drivers that Next.js already keeps external, such as pg and mongodb, don't need adding. On Next.js 16, use agent 3.5.4 or later, which instruments them when you run next start. A client the agent has no hook for, such as postgres.js or Prisma, stays missing.

Does the agent work on the Edge runtime?

No. The Atatus Node.js agent runs on the Node.js runtime, and the onRequestError example skips Edge requests. Code that runs on the Edge runtime isn't covered.

Do I need both server and browser monitoring for Next.js?

If you want the full picture, yes. Server monitoring shows how long each route takes to render and why it fails. Browser monitoring shows what happens after the HTML arrives: when content appears, how quickly the page responds, and the errors users hit in Client Components.

Atatus shows both sides of a Next.js app in one place: route timing, traces and server errors from Node.js APM, and route changes, Web Vitals and JavaScript errors from real user monitoring.

← All posts

Get started

See the root cause first.

Start free and send your first trace with OpenTelemetry or the Atatus agents. APM, logs, RUM, infrastructure and SIEM, all on one timeline.

  • SaaS
  • On-premises
  • Hybrid
  • BYOC
  • No per-user pricing
Keep reading