afaqrashid.com  ·  portfolio · 2026 ed.

Next.js "Dynamic Server Usage" Error (DYNAMIC_SERVER_USAGE): Tested

During next build, Next.js tries to prerender every page as static HTML. When a page calls cookies(), headers() or fetch with cache: "no-store", Next.js throws a special error to stop the prerender and mark the route dynamic. That error is "Dynamic server usage", with the digest DYNAMIC_SERVER_USAGE. You normally never see it. You see it when your own code catches it.

I tested the cases on Next.js 16.4.4: catching it and logging, catching it and swallowing it, dynamic = "error", force-static, unstable_cache, and cookies() outside a request.

What I found

code                                              build        route    result
headers() / fetch no-store in a page              passes       dynamic  fine
same, inside try/catch that logs the error        passes, logs dynamic  fine, noisy build log
same, try/catch + unstable_rethrow(e)             passes       dynamic  fine, no log
headers() in try/catch + dynamic = "error"        passes       static   error text baked into the HTML
cookies() with dynamic = "error", no try/catch    fails        -        clear error
cookies() with dynamic = "force-static"           passes       static   cookies always empty
cookies() inside unstable_cache                   throws       -        clear error
cookies() at module scope                         fails        -        "called outside a request scope"
cookies() in generateStaticParams                 fails        -        clear error

1. The log in your build output

export default async function Page() {
  let ua = "unknown";
  try {
    ua = (await headers()).get("user-agent") ?? "none";
  } catch (e) {
    console.error("headers failed:", e);
  }
  return <p>ua={ua}</p>;
}
headers failed: Error: Dynamic server usage: Route /d-headlog couldn't be rendered
statically because it used `headers`. See more info here:
https://nextjs.org/docs/messages/dynamic-server-error
  digest: 'DYNAMIC_SERVER_USAGE'

The build still passed, the route was marked dynamic (ƒ), and at runtime the page showed the real user agent. So this one is noise: Next.js's own stop signal passed through your catch and your console.error printed it. A fetch with cache: "no-store" inside try/catch gave the same message, worded "because it used revalidate: 0 fetch https://example.com/".

Don't fix it by hiding the log. Let Next.js's internal errors through with unstable_rethrow before your own handling:

import { headers } from "next/headers";
import { unstable_rethrow } from "next/navigation";

try {
  ua = (await headers()).get("user-agent") ?? "none";
} catch (e) {
  unstable_rethrow(e); // rethrows Next.js's own errors, including notFound() and redirect()
  console.error("headers failed:", e);
}

With that line the build log was clean and the route was still dynamic. Better still, keep cookies(), headers() and fetch out of try/catch blocks that also handle your own errors.

2. When try/catch ships the error to your users

The dangerous version is the same try/catch on a route that also says export const dynamic = "error" (often added to make sure a page stays static). The build passed and the route stayed static, but the catch had swallowed the stop signal, so the prerendered HTML contained the error text itself:

<p>ua=caught: Route /d-catch with `dynamic = "error"` couldn't be rendered ...</p>

Every visitor got that, from a static file. Without the try/catch, the same page failed the build with a clear message: "Route /d-gsp/[id] with `dynamic = "error"` couldn't be rendered statically because it used `cookies()`". That failure is the one you want.

3. force-static turns cookies into nothing

With export const dynamic = "force-static", cookies() didn't throw at all. The page built as static, and when I requested it with a theme=dark cookie it still said theme=none. force-static makes cookies, headers and searchParams return empty values. Use it only on pages that don't need them.

4. cookies() inside unstable_cache

Calling cookies() inside unstable_cache threw: "cookies() can't be read inside unstable_cache(). Read it outside the cached function and pass what you need as an argument." A cached function's result is shared between users, so it can't depend on one request's cookies. Read the cookie in the page and pass the value in.

"cookies was called outside a request scope"

This is a different error with the same root: there's no request to read from. I got it in two places. At module scope, a top-level cookies() call outside any function failed the build with "`cookies` was called outside a request scope". In generateStaticParams, it failed with "Route /d-gsp2/[id] used `cookies()` inside `generateStaticParams`. This is not supported because `generateStaticParams` runs at build time without an HTTP request."

The same applies to scripts, cron jobs, and anything else that imports your server code and runs outside a page, layout, route handler, server action or proxy (middleware). Move the cookies() or headers() call into the request-handling function and pass the value down to whatever needs it.

Checklist

  • Seeing "Dynamic server usage" in the build log with a passing build: your try/catch is logging Next.js's signal. Add unstable_rethrow(e).
  • Never put dynamic = "error" and a try/catch around cookies() or headers() on the same page: check the built HTML.
  • force-static means empty cookies and headers.
  • Read cookies outside cached functions and pass the value in.
  • "Outside a request scope": the call is at module level, in generateStaticParams, or in code run outside a request. Move it into the page or handler.

Related

Sources