Monitoring and Debugging in Production
AdvancedErrors 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.
// 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.
// 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.
// 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 AriaWhy is the error digest important in production?
What kinds of errors appear only in client-side monitoring?
Why must caching bugs be reproduced with a production build?
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.