Next.js

Fix Dynamic Server Usage Error in Next.js App Router

Next.js throws this when a route it wants to render at build time calls a dynamic function — cookies(), headers(), searchParams, or a no-store fetch. The fix is either opt the route into dynamic rendering, or remove the request-time dependency if it should be static.

Mahdi Ben RhoumaMahdi Ben Rhouma
Updated
9 min read

Your build fails, or a route logs at runtime:

text
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?

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() and headers() (from next/headers)
  • searchParams (the page prop)
  • draftMode()
  • a fetch(..., { cache: 'no-store' }) (or revalidate: 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)#

js
// 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:

Don't
export const dynamic = 'force-dynamic' // slapped on to silence the error, even though the page is the same for everyone
Do
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 to cache: 'force-cache' or revalidate: N.
  • Want it fully static? export const dynamic = 'force-static' forces cookies(), headers(), and useSearchParams() 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:

tsx
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <>
      <StaticHeader />
      <Suspense fallback={<Skeleton />}>
        <UserPanel />   {/* this one reads cookies() */}
      </Suspense>
    </>
  )
}
Honest nuance

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#

js
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.

Version caveat — Next.js 15 made request APIs async

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.

When this won't work
  • You call cookies()/headers() inside setTimeout/setInterval or 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/revalidate route 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.

Declaring dynamic intent explicitly with connection() and unstable_noStore()#

The three fixes above all route through the same mechanism — export const dynamic segment config, or removing/keeping a dynamic function call. But sometimes neither fits: you want the route to be dynamic for a reason that has nothing to do with cookies, headers, or fetch caching — for example, a component that reads Math.random() or the current timestamp and must never be statically frozen at build time. Reaching for a fake cookies() call just to trigger dynamic rendering is a common workaround, and it's exactly the kind of stray dependency Fix 2 above tells you to remove.

Next.js has two purpose-built APIs for this instead of a segment-wide export:

tsx
import { unstable_noStore as noStore } from 'next/cache'
 
export default async function Component() {
  noStore() // opts this data access out of caching, without a fake dynamic function
  const data = await getData()
  return <div>{data}</div>
}

unstable_noStore() marks the surrounding scope as needing fresh data on every render — functionally equivalent to cache: 'no-store' on a fetch, but usable around any async call, not just fetch. The newer connection() API (from next/server) is the more explicit successor: awaiting it tells Next.js "this component needs the incoming request to exist before it can render," which forces dynamic rendering the same way cookies() does, without touching any actual request data. Both are meant for exactly the case this error covers — a route that legitimately can't be prerendered — and are the cleaner alternative to the workaround this page's Fix 1 describes. For the caching model these interact with, see Next.js 15 Caching, Explained.

generateStaticParams and this error on the same route#

A route with a dynamic segment (app/blog/[slug]/page.tsx) that exports generateStaticParams tells Next.js which param values to prerender at build time — but that's independent of whether the rendered page also calls a dynamic function. It's entirely possible to prerender every known slug and still hit this error, if the page component also reads searchParams or calls cookies() for something like a draft-preview check.

What actually happens: Next.js prerenders each param from generateStaticParams first. If, during that prerender, the component tree hits an uncaught dynamic API call, you get the same dynamic-server-usage error described above — the static param list didn't prevent it, because the two mechanisms answer different questions (generateStaticParams: which slugs exist; dynamic functions: can this render without a live request). The dynamicParams config only controls what happens for slugs not in the returned list (true renders them on demand, false returns a 404) — it has no effect on whether a listed slug's own render is static or dynamic.

The practical fix is the same decision tree as the rest of this page: if the per-request check (draft mode, a preview cookie) is genuinely needed on every visit, accept the dynamic render for that segment; if it's only relevant to a small admin-only path, split it into its own route so the public, statically-generated pages stay fast. For the broader tradeoffs of prerendering strategy in the App Router, see Next.js Pages Router to App Router migration guide.

Further reading: Fix ChunkLoadError: Loading Chunk Failed in Next.js. Also see Fix Next.js: Unable to Acquire the Dev Server Lock.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.