Next.js logging in production: where your logs go, and how not to lose your errors
Why your console.log doesn't show up where you expect, how to write logs you can search, how to catch server and browser errors with instrumentation.ts, and what happens to your logs on Vercel. For Next.js 15 and 16, with the App Router.
1. Where does each log go?
In Next.js your code runs in three places, and each one writes its logs somewhere different. Most of the confusion comes from here:
| Your code | Runs on… | Its console.log shows up in… |
|---|---|---|
Server Components, page.tsx and layout.tsx without 'use client' | The server | The next dev terminal; in production, your server's logs or Vercel's |
Route Handlers (route.ts) and Server Actions | The server | Same: terminal or server logs |
proxy.ts (formerly middleware.ts) | The server, before each route | Server logs; on Vercel, as «Routing Middleware» |
Components with 'use client' | The server (first render) and the browser | Each visitor's browser console, which nobody else sees |
next build | Your CI or Vercel | The build log (static pages are rendered here) |
In Next.js 16, middleware.ts is called proxy.ts and runs on Node.js by default.
2. Why can't I see my console.log?
The three most common reasons:
- You're looking in the browser and it's a Server Component. Without
'use client', the component runs on the server: its log goes to the terminal, not the browser's dev tools. - The page is static. If the page doesn't read anything from the request, Next.js renders it once during
next build. Its log showed up in the build and won't show up again on each visit. - It's a client component in production. The log goes to the visitor's browser. If it breaks for a customer, you don't see it. That's what step 5 is for.
3. Searchable logs with pino
console.log writes free text, with no level and no way to tell which request it came from. On the server, use pino: it writes JSON, and Next.js already keeps it out of its bundle (pino and pino-pretty are on its serverExternalPackages list), so it works with no configuration.
npm install pino
npm install --save-dev pino-pretty
// lib/logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// readable in your terminal; plain JSON in production
transport: process.env.NODE_ENV === 'production' ? undefined : { target: 'pino-pretty' },
redact: ['*.password', '*.token', '*.card'],
});
// app/api/orders/route.ts
import { logger } from '@/lib/logger';
export async function POST(req: Request) {
const log = logger.child({ route: '/api/orders', request: req.headers.get('x-vercel-id') });
const order = await req.json();
log.info({ order: order.id, amount: order.total }, 'order received');
// …
}
Two Next.js-specific details:
- Use the logger only in server code. If you import it in a
'use client'component, Next.js tries to ship it to the browser. To make that fail at build time instead of in production, putimport 'server-only'at the top oflib/logger.ts. - Each request's id. On Vercel, the
x-vercel-idheader identifies the request and is the one you see in its dashboard. On your own server, use your load balancer's (x-request-id) or the W3Ctraceparent.
4. Server errors: onRequestError
In production, when a Server Component throws, Next.js sends the browser a generic message and a digest, a code that identifies the error: it doesn't show the visitor your message or your stack. The full error only stays on the server, one line among thousands.
Since Next.js 15, instrumentation.ts (at the project root, or in src/) has a hook that receives every server error: from Server Components, Route Handlers, Server Actions and proxy.ts.
// instrumentation.ts
import type { Instrumentation } from 'next';
export function register() {
// runs once, when each server starts
}
export const onRequestError: Instrumentation.onRequestError = async (err, request, context) => {
const { logger } = await import('./lib/logger');
logger.error({
err,
digest: (err as { digest?: string }).digest, // the same code the browser saw
route: context.routePath, // app/orders/[id]/page
type: context.routeType, // render, route, action or proxy
method: request.method,
}, 'server error');
};
With the digest in your log, when a customer sends you a screenshot of «Application error» with its code, you search for it and land on the error with its stack.
If anything in onRequestError is async, await it: Next.js doesn't wait for what you leave dangling.
5. Browser errors: error.tsx
What breaks in a client component never reaches onRequestError: it happens in the browser. Next.js catches it with the nearest error.tsx (and global-error.tsx if the root layout breaks), which is a client component. You can report it from there:
// app/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
// send the error wherever you'll look at it (your API, or an error service)
navigator.sendBeacon('/api/errors', JSON.stringify({
message: error.message, digest: error.digest, page: location.pathname,
}));
}, [error]);
return <button onClick={reset}>Something went wrong. Try again</button>;
}
If the error came from the server, error.digest is the same code that's in your log from step 4. If it started in the browser, there's no digest and all you have is what you send from here.
One detail: error.tsx only catches render errors. An error in an onClick or a setTimeout doesn't go through it; for those you need to listen to window.onerror and unhandledrejection.
6. On Vercel: one hour or one day
Vercel groups your logs by request and lets you search them in its dashboard (Logs) or with vercel logs. What almost nobody checks until they need it is how long it keeps them:
| Vercel plan | Your logs last |
|---|---|
| Hobby | 1 hour |
| Pro | 1 day |
| Pro with Observability Plus | 30 days |
| Enterprise | 3 days (30 with Observability Plus) |
And per request: up to 256 lines, up to 256 KB each, and 1 MB in total; past that, only the most recent remain. Free-text search looks at the message and the path, not at your JSON fields.
So if a customer writes on Monday about something that happened on Saturday, on Hobby and Pro that log no longer exists. To keep them longer you have to send them somewhere else: with a Vercel log drain or from your own code.
Limits from Vercel's Runtime Logs documentation, checked on October 18, 2026.
7. Logs that go out after you respond: after()
On Vercel (and any serverless platform), your function can be frozen as soon as it responds. Whatever it was sending in the background, a log to another service for example, never goes out. That's why so many logs «get lost» right in production.
Since Next.js 15.1, after() schedules work for after the response, and the platform waits for it to finish (on Vercel, with waitUntil). The visitor doesn't wait, and the log does go out:
import { after } from 'next/server';
export async function POST(req: Request) {
const order = await charge(await req.json());
after(() => sendLog({ order: order.id, amount: order.total })); // after responding
return Response.json({ ok: true });
}
after() works in Server Components, Route Handlers, Server Actions and proxy.ts, and runs even if the response ended in an error.
8. What doesn't belong in your logs
- Passwords, tokens, cookies and keys. In Next.js, be careful logging all of
headers()orcookies(): that's where the session lives. - Environment variables. A
console.log(process.env)on the server leaves all your keys in the log. And anything starting withNEXT_PUBLIC_reaches the browser: never put a secret there. - Card numbers and personal data you don't need. The customer's id is enough.
9. Have someone read them for you
With all of the above your logs are searchable, but nobody is watching them, and on Vercel they last an hour or a day. What's missing is finding out without opening them: the new error, the route that started failing, the paid order that never shipped.
With Estiaje it's instrumentation.ts plus one line per route:
npm install @estiaje/sdk
// lib/estiaje.ts
import { estiajeFromEnv } from '@estiaje/sdk';
export const estiaje = estiajeFromEnv({ service: 'store-web' });
// instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
const { estiaje } = await import('./lib/estiaje');
estiaje.capturarConsola(); // your console.log, as is
estiaje.capturarErrores(); // uncaught errors
}
}
export async function onRequestError(err: unknown, request: { path: string }, context: { routePath: string; routeType: string }) {
if (process.env.NEXT_RUNTIME !== 'nodejs') return;
const { estiaje } = await import('./lib/estiaje');
estiaje.error('server error', { route: context.routePath, type: context.routeType }, err); // with its stack
await estiaje.flush();
}
// in a route: the process that must finish, sent even if the function freezes
estiaje.event('order.paid', { order_id: order.id, amount: order.total });
after(() => estiaje.flush());
Your logs are searchable by text and filterable by service, level or any field, for more than a day. Errors arrive grouped with their stack, and browser errors too, with one <script> tag (the full Next.js recipe). If you tell it that every order.paid must end in order.shipped, it alerts your phone about the one that got stuck halfway, with the money at risk. Logs are included from the Free plan.
Frequently asked questions
Where do I see Next.js logs in production?
Server logs (Server Components, Route Handlers, Server Actions and proxy) go to your server's standard output: with Docker, in docker logs; with pm2, in pm2 logs; on Vercel, in your project's Logs section or with vercel logs. Client component logs go to each visitor's browser.
Why doesn't my console.log show up in the browser?
Because the component runs on the server. In the App Router, every component without 'use client' is a Server Component and its log goes to the next dev terminal or the server logs. If the page is static, the log appeared once, during next build.
How long does Vercel keep logs?
One hour on Hobby, one day on Pro and three days on Enterprise; 30 days with Observability Plus. Also, up to 256 lines per request. To keep them longer, send them to another service with a log drain or from your code.
What is the digest of a Next.js error?
In production, Next.js doesn't show the browser the message of a server error: it sends a generic message and a code, the digest. The same code reaches onRequestError in instrumentation.ts; if you store it in your log, you can go from the customer's screenshot to the error with its stack.
Can I use pino with Next.js?
Yes, in server code. Next.js already keeps pino and pino-pretty out of its bundle, so you don't need to configure serverExternalPackages. Don't import it in client components; import 'server-only' in your logger file prevents that.
Next.js 16.4 and Next.js and Vercel documentation, checked on October 18, 2026.