Blog · October 20, 2026

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 codeRuns on…Its console.log shows up in…
Server Components, page.tsx and layout.tsx without 'use client'The serverThe next dev terminal; in production, your server's logs or Vercel's
Route Handlers (route.ts) and Server ActionsThe serverSame: terminal or server logs
proxy.ts (formerly middleware.ts)The server, before each routeServer logs; on Vercel, as «Routing Middleware»
Components with 'use client'The server (first render) and the browserEach visitor's browser console, which nobody else sees
next buildYour CI or VercelThe 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:

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:

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.

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 planYour logs last
Hobby1 hour
Pro1 day
Pro with Observability Plus30 days
Enterprise3 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

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.

Try 14 days of Pro, no card When it ends, you stay on Free with your logs and errors.

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.

Keep reading