The Next.js App Router lets a page or layout export a route segment config named dynamic. What are its four values, and what does each one do to a page that calls headers()?
answer
- auto, force-dynamic, force-static, error
- default infers; the rest override
- one value empties the dynamic APIs
- one value fails the build instead
- revalidate = 0 forces dynamic too
basics
~10 sThe values are 'auto', 'force-dynamic', 'force-static' and 'error'. Auto lets headers() opt the route into dynamic rendering; force-dynamic renders per request regardless; force-static prerenders and makes headers() return empty values; error fails the build.
solid answer
~40 s`export const dynamic` takes four values. `'auto'` is the default: Next prerenders what it can, so a `headers()` call quietly switches the route to dynamic rendering. `'force-dynamic'` renders the route on every request whether or not anything asks for the request. `'force-static'` forces prerendering and makes the Dynamic APIs return empty values — `headers()` and `cookies()` come back empty and `useSearchParams()` yields nothing — so the page builds but the code reading them silently sees nothing. `'error'` also forces static rendering, but instead of emptying those APIs it fails the build and names the offending call. There is also `export const revalidate = 0`, which is equivalent to forcing dynamic rendering, and `fetchCache` for overriding fetch caching across a segment.
code
typescript · 8 lines// app/(marketing)/pricing/page.tsx
// Guard: if anything here starts reading the request, fail the build.
export const dynamic = 'error'
export default async function Pricing() {
const plans = await getPlans()
return <ul>{plans.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}go deeper
Know that a page or layout can export const dynamic and that 'force-dynamic' means render every request while 'force-static' means prerender regardless. Recognise 'auto' as the default you normally have.
Be able to contrast 'force-static' and 'error' precisely: both prerender, but one empties the Dynamic APIs silently and the other fails the build naming the call. Mention revalidate = 0 as the other dynamic-forcing export.
Show judgment about when config is warranted at all, and use 'error' as a diagnostic to make a mysteriously dynamic build tell you which call did it.
Argue for config as a stated contract on the routes that matter rather than a sprinkling of overrides, and treat 'force-static' on a request-reading route as a correctness bug waiting to ship.
## The escape hatch above the implicit rules By default Next.js infers a route's rendering mode from what the code does: touch a Dynamic API and the route becomes dynamic, touch none and it is prerendered. Route segment config is the layer that overrides the inference. You export a `const` from a `page.tsx` or `layout.tsx`, and it applies to that segment and everything below it. ```tsx // app/reports/page.tsx export const dynamic = 'force-dynamic' export const revalidate = 0 ``` ## The four values of `dynamic` **`'auto'`** — the default. Next caches and prerenders as much as it can without stopping any component from opting into dynamic behaviour. A `headers()` call in this route works normally and takes the whole route dynamic. This is the mode you are in whenever you have not thought about it. **`'force-dynamic'`** — the route is rendered for every request, full stop, even if nothing in it reads the request. Use it when the render has a side effect or a freshness requirement the framework cannot infer. Historically this also pushed fetch requests in the segment away from caching, which is one more reason to say which version you assume when discussing it. **`'force-static'`** — the opposite: the route is prerendered no matter what, and the Dynamic APIs are neutralised rather than honoured. `cookies()` and `headers()` return empty values and `useSearchParams()` yields empty search params. This is the dangerous one. Your code still compiles, the build still succeeds, and the page still renders — with everyone treated as signed out, because the cookie read came back empty. If you reach for `'force-static'`, you are asserting that nothing in the route legitimately needs the request, and you should test the signed-in path to prove it. **`'error'`** — also forces static rendering, but it refuses to paper over the conflict. If any component in the route uses a Dynamic API or an uncached data read, the build fails and points at it. This makes it both a safety rail and a diagnostic tool: set it temporarily on a route that mysteriously went dynamic and the build will name the culprit. ## `force-static` versus `error`: the distinction interviewers probe Both force static rendering. They differ entirely in what happens when the code disagrees: | Config | Route prerendered? | A `headers()` call in it | | --- | --- | --- | | `'force-static'` | yes | returns empty values, silently | | `'error'` | yes | build fails, naming the call | `'force-static'` trades correctness for a green build; `'error'` trades a green build for correctness. For a route that must stay static — a landing page, a docs page — `'error'` is almost always the better choice, because a route that quietly starts treating every visitor as anonymous is worse than a build that stops. ## The neighbouring exports `dynamic` does not travel alone. `export const revalidate = 0` on a segment guarantees dynamic rendering even when no Dynamic API is present, so it overlaps `'force-dynamic'` for that purpose; non-zero values are about refresh cadence rather than rendering mode. `export const fetchCache` overrides how `fetch` calls within the segment are cached, which is the sharpest tool in the box and the one most likely to surprise the next reader. `export const dynamicParams` controls what happens for path values that were not prerendered. All of these are ordinary module exports read at build time, so they must be statically analysable — you cannot compute them at runtime, and you cannot set them from a Client Component. ## When to reach for any of this The honest answer in an interview is: rarely, and deliberately. Config that forces a mode is a claim about the route that the code itself no longer expresses, so it drifts. The two uses that hold up are `'error'` as a permanent guard on routes whose static-ness matters, and `'force-dynamic'` where the freshness requirement genuinely cannot be inferred from the code. Reaching for `'force-static'` to silence a build classification you did not like is how apps end up serving signed-out pages to signed-in users.
- Why is 'force-static' considered risky on a route that reads cookies?Because it does not remove the read, it neutralises it. `cookies()` returns an empty store, so the page renders successfully with every visitor treated as signed out. Nothing errors, the build is green, and the bug surfaces in production as wrong content rather than as a failure.
- Where would you put dynamic = 'error', and why there rather than on every route?On the routes whose static rendering is load-bearing — landing pages, docs, high-traffic marketing paths. It turns an accidental dynamic read into a build failure with a name attached. Applying it everywhere just blocks legitimately dynamic routes, so it is a guard for the pages you have decided must never regress.
- Can you decide the value of these config exports at runtime?No. They are read during the build by static analysis, so they must be literal values in the module. That is also why they cannot come from a Client Component or an environment lookup evaluated at request time — if you need per-request behaviour, that is what the Dynamic APIs and `connection()` are for.
saying these in an interview costs you the question
- Thinks force-static makes cookies() throw rather than return empty
- Confuses dynamic = 'error' with a runtime error page
- Believes force-dynamic is required for any page that fetches data
- Says the config can be computed at runtime from an env value
- Treats force-static as a safe way to silence a dynamic route