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 error1. 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
- params should be awaited: tested on Next.js 16
- useSearchParams() should be wrapped in a Suspense boundary
- generateMetadata and page fetching twice
- Next.js development