Home/Learn/Next.js/Monitoring and Debugging in Production

Monitoring and Debugging in Production

Advanced
Production

Errors happen on two sides of a boundary and in short-lived functions. Without a request id joining them, a production bug is a guess.

Overview

Debugging a Next application in production is harder than a traditional server for a specific reason: the same request can involve a client component, a server component, a server action and a route handler, each logging separately, in a function that no longer exists by the time you look. The fix is the same as in the Full-Stack track — a request id carried through everything and attached to every log line and error report — plus knowing which of the framework's own signals are worth watching.

Errors From Both Sides

Server and client capture, joined by one identifier.

instrumentation.ts, the digest, and client capture
// Server errors are redacted in production: the client sees a
// generic message and a digest, and the full stack is in your logs
// under that digest. Show the digest, or the user has nothing to
// quote to support.
export default function Error({ error }) {
  return <p>Something went wrong. Reference: {error.digest}</p>
}

// Capture both sides. Sentry's Next SDK wires server components,
// actions, route handlers and the browser:
// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') await import('./sentry.server.config')
  if (process.env.NEXT_RUNTIME === 'edge')   await import('./sentry.edge.config')
}
export const onRequestError = Sentry.captureRequestError

// What client-side capture catches that server logs never will:
//   hydration mismatches
//   chunk load failures after a deploy
//   errors in event handlers, which no boundary catches
//   failures on browsers you do not test

// A global handler for unhandled rejections, so they are at least
// counted:
window.addEventListener('unhandledrejection', (e) => report(e.reason))

Structured Logs and a Request Id

The one habit that makes production debuggable.

One id, generated at the edge, logged everywhere
// middleware.ts — generate once, at the edge of the system
const id = req.headers.get('x-request-id') ?? crypto.randomUUID()
const headers = new Headers(req.headers)
headers.set('x-request-id', id)
const res = NextResponse.next({ request: { headers } })
res.headers.set('x-request-id', id)        // back to the client too

// Read it anywhere on the server
const requestId = (await headers()).get('x-request-id')

// Log structured objects, not strings — a serverless log sink cannot
// grep a sentence usefully:
console.log(JSON.stringify({
  level: 'info', event: 'problem_published',
  requestId, userId: user.id, slug, ms: Date.now() - started,
}))

// Forward it to your backend so both systems share one identifier
// (see the Full-Stack Integration track — this is the same id).
headers: { 'x-request-id': requestId, cookie: cookies().toString() }

// Never log: request bodies of auth or payment endpoints, tokens,
// full cookie headers, or personal data you would not put in an email.

// Vercel keeps runtime logs for a limited window on lower plans, so
// drain them somewhere durable if you need history.

What to Watch

Signals that predict a problem rather than confirm one.

Route-level errors, p95, and build output drift
// Framework-specific:
//   the static/dynamic mix in the build output — a route flipping to
//     dynamic raises cost and latency silently
//   function duration and invocation count per route
//   cold start frequency
//   ISR cache hit rate, if you self-host
//   image transformation count, if you pay per transform

// Application:
//   error rate by route, not just overall
//   p95 and p99 latency, never the average
//   Core Web Vitals from the field
//   one business metric — signups, submissions — because "no errors
//     but nobody can check out" is a real and silent failure

// Alert on the SHAPE of failures:
//   a spike in 401s      -> auth broke, or a token change shipped
//   a spike in 422s      -> the frontend started sending something new
//   a spike in 404s      -> a broken link or a bad deploy
//   a spike in timeouts  -> an upstream dependency is degrading

// And the local reproduction rule that saves the most time:
npm run build && npm start
// The dev server does not cache, does not prerender, and double-
// renders. Anything about caching, staleness or a production-only
// error must be reproduced against a real build first.

Key Points to Remember

  • 1Production redacts server errors to a message and a digest — surface the digest so support can match a log line
  • 2instrumentation.ts plus onRequestError wires capture across server components, actions, handlers and edge
  • 3Client-side capture catches hydration mismatches and post-deploy chunk failures that server logs never see
  • 4Generate a request id in middleware, log it structurally, and forward it to your backend
  • 5Watch route-level error rates, p95 latency and the static/dynamic mix; reproduce caching bugs against a real build

Interview Questions

Sign in to ask Aria
1

Why is the error digest important in production?

Medium
2

What kinds of errors appear only in client-side monitoring?

Hard
3

Why must caching bugs be reproduced with a production build?

Medium

Ask Aria about Monitoring and Debugging in Production

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…