Your build fails, or a route logs at runtime:
Dynamic server usage: Route /dashboard couldn't be rendered statically
because it used `cookies`.
(Swap cookies for headers, searchParams, or no-store fetch.) This is DYNAMIC_SERVER_USAGE. It is not a bug — it's Next.js telling you a route it wanted to prerender at build time depends on request-time data. The fix is a decision: should this route be dynamic, or should it not need that data at all?
{ name: "Next.js", version: "App Router 13.4–15" },
{ name: "Rendering", version: "static vs dynamic" },
]} />
The mental model: build time vs request time
- Static rendering = the route is rendered at build time, cached, CDN-shareable.
- Dynamic rendering = the route is rendered per request.
The following are dynamic functions — they depend on the incoming request and force dynamic rendering:
-
cookies()andheaders()(fromnext/headers) -
searchParams(the page prop) draftMode()- a
fetch(..., { cache: 'no-store' })(orrevalidate: 0)
Per the docs: "During rendering, if a dynamic function or uncached data request is discovered, Next.js will switch to dynamically rendering the whole route." Normally that switch is automatic and silent — Next throws DynamicServerError internally and catches it. You see the error "when it's uncaught" — typically because the route was forced static (dynamic = 'error', a static route handler, generateStaticParams, sitemap/metadata files) while a child still calls a dynamic API, or the call ran outside Next's tracked async context.
Fix 1 — Opt into dynamic rendering (when the data IS request-specific)
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
The docs describe this as forcing "routes being rendered for each user at request time... equivalent to getServerSideProps() in the pages directory." This is the right fix when the route genuinely needs per-request data (the user's session, a header, query params).
export const revalidate = 0 achieves the same dynamic outcome via the caching model. Both are valid; force-dynamic is the more explicit switch.
Fix 2 — Remove the dynamic dependency (when it SHOULD be static)
If the data isn't actually request-specific, don't go dynamic — fix the cause so the route stays fast and cacheable:
wrong="export const dynamic = 'force-dynamic' // slapped on to silence the error, even though the page is the same for everyone"
right="Drop the stray cookies()/headers() call, or add caching to the fetch — let the route prerender."
/>
- Reading a cookie you don't need? Remove the
cookies()call. - Fetching with
cache: 'no-store'for data that rarely changes? Switch tocache: 'force-cache'orrevalidate: N. - Want it fully static?
export const dynamic = 'force-static'forcescookies(),headers(), anduseSearchParams()to return empty values.
Fix 3 — Isolate the dynamic part in Suspense
Keep most of the page static and stream only the request-time piece:
import { Suspense } from 'react'
export default function Page() {
return (
<>
<StaticHeader />
<Suspense fallback={<Skeleton />}>
<UserPanel /> {/* this one reads cookies() */}
</Suspense>
</>
)
}
Without Partial Prerendering, a dynamic API still makes the whole route dynamic — Suspense buys you streaming, not a static shell. With PPR enabled it becomes the canonical way to keep a static shell plus a dynamic hole. Don't oversell Suspense as a static-shell fix on stable Next 14/15 without PPR.
Route segment config reference
export const dynamic = 'auto' // 'auto' | 'force-dynamic' | 'error' | 'force-static'
export const revalidate = false // false | 0 | number (Node.js runtime only)
export const dynamicParams = true // true | false
Values must be statically analyzable — revalidate = 600 is valid, revalidate = 60 * 10 is not.
In Next.js 15, cookies, headers, draftMode, params, and searchParams became asynchronous — you must await them: const cookieStore = await cookies(). In 13.4/14 they were synchronous. There's a codemod: npx @next/codemod@canary next-async-request-api .. Also note Next 15 stopped caching GET route handlers and client navigations by default, which shifts where this error shows up.
- You call
cookies()/headers()insidesetTimeout/setIntervalor after an un-awaited promise — that's a separate async-context bug; read the value first, then enter the deferred context. - You're on Next.js 16 with Cache Components enabled — the
dynamic/revalidateroute configs are removed there; this guide targets stable Next 14/15 semantics.
Official references: dynamic-server-error, app-static-to-dynamic-error, route segment config, Next.js 15 blog.
Related Articles
- Next.js 15 Caching, Explained
- Fix Stale Cache and Revalidation in Next.js
- Fix revalidatePath Not Working in Server Actions
- Next.js App Router Complete Guide
Frequently Asked Questions
What causes "Dynamic server usage" in Next.js?
A route Next wanted to prerender called a dynamic function — cookies(), headers(), draftMode(), searchParams, or a no-store fetch — which needs the incoming request. Next normally switches to dynamic rendering automatically; the error surfaces when that switch is blocked or the call ran outside Next's tracked context.
How do I make a Next.js route dynamic?
Add export const dynamic = 'force-dynamic' (or revalidate = 0) to the segment. It renders per request — the App Router equivalent of getServerSideProps.
Should I always add force-dynamic?
No. If the data isn't request-specific, remove the dynamic dependency so the route stays static and fast. force-dynamic is right only when the route must render per request.
Originally published at https://www.iloveblogs.blog
Top comments (0)