The detector reads your code
Next.js decides per route whether to render at build time (static) or per request (dynamic). It doesn't ask you — it watches which APIs you call:
| API | Effect |
|---|---|
cookies() | Forces dynamic |
headers() | Forces dynamic |
searchParams | Forces dynamic |
connection() | Explicit opt-in to dynamic |
fetch() without cache | Treated as dynamic data point |
Override when you must
Sometimes the detector picks the wrong default for what you actually want. export const dynamic = 'force-static' errors if the route uses a dynamic API; 'force-dynamic' guarantees per-request rendering even when nothing dynamic is in the code.
Why this matters for cost
A route that's static can be served from CDN with no compute cost per request. A dynamic route runs your code every time. Pick on purpose.
Treat the build route table as an operational artifact. When a supposedly static route becomes dynamic, trace cookies(), headers(), search params, and uncached fetches through its ancestors. A flag check placed in a broad layout can change the cost model of every descendant. When a route classification surprises you, trace the API call that introduced the dynamic signal instead of patching only the result. That explanation makes later build changes auditable.
force-static and force-dynamic are contracts, not spells for defeating the detector. Use them to make intent fail loudly. Forcing a route against its real inputs yields either incorrect output or avoidable compute. Add each dynamic signal one at a time and record the build classification. Place one signal in a shared layout to measure the blast radius. A quarterly route-cost map should explain every unexpected dynamic branch.