The Next.js dynamic APIs are functions that read data only available when a real request arrives. They are cookies(), headers(), searchParams, and draftMode(), plus route params that were not prerendered. Reading one used to flip the whole route to dynamic rendering, and with Cache Components the behavior is more surgical.
What counts as a dynamic API
The request-time APIs all read something that is different per visitor or per URL. Next.js treats them as the line between what can be prerendered and what must wait for a request. Anything past that line ships at request time.
cookies()reads the visitor's cookie jar.headers()reads the incoming request headers.searchParamsreads the query string of the URL.draftMode()checks whether draft preview is enabled.
Dynamic route params behave the same way when generateStaticParams did not prerender that value at build time. The difference is what the data depends on: cookies and headers vary per visitor, while search params and route params vary per URL. Either way, the read is what forces Next.js to wait for a request instead of serving a stored page.
The pre-Cache-Components behavior
In the model before Cache Components, a single read opts the entire route into dynamic rendering. The page renders per request and the build output marks it with the dynamic symbol. There is no way to keep the rest of the page static once one part reads a cookie.
// app/profile/page.tsx
import { cookies } from 'next/headers'
export default async function ProfilePage() {
const theme = (await cookies()).get('theme')?.value ?? 'light'
return <p>Theme: {theme}</p>
}After next build, this route is listed with the ƒ dynamic symbol instead of ○. The cookie read is the only reason, and it costs the whole page its static output.
The Cache Components behavior
With Cache Components enabled, the same read only makes the boundary around it stream. Wrap the read in a Suspense boundary and the rest of the page still ships in the static shell.
// app/profile/page.tsx
import { cookies } from 'next/headers'
import { Suspense } from 'react'
async function Theme() {
const theme = (await cookies()).get('theme')?.value ?? 'light'
return <p>Theme: {theme}</p>
}
export default function ProfilePage() {
return (
<main>
<h1>Profile</h1>
<Suspense fallback={<p>Loading theme...</p>}>
<Theme />
</Suspense>
</main>
)
}The heading and fallback prerender into the shell. Only the theme line waits for the request, because the cookie read now sits inside a boundary. Cached content still joins the shell too, so a page can mix static, cached, and streamed parts.
In the build output this route reports ◐ rather than ƒ, which is the signal that a shell was prerendered and only part of the page streams. Leaving the read outside a boundary is not just slower here, it fails the build with a prerender-blocking error.
For deliberately forcing request-time rendering without one of these APIs, see the connection() function.
force-static returns empty values
The old model has one escape hatch. Setting dynamic = 'force-static' forces the route to prerender and makes cookies(), headers(), and useSearchParams() return empty values. It is a blunt tool, and it only exists in the pre-Cache-Components model.
For tracking down why a route turned dynamic, see why a route became dynamic.
Rune AI
Key Insights
- cookies(), headers(), searchParams, and draftMode() are request-time APIs.
- Reading one opts the route out of static rendering in the old model.
- With Cache Components, only the Suspense boundary streams.
- Dynamic route params behave the same when generateStaticParams does not cover them.
- force-static in the old model makes these APIs return empty values.
Frequently Asked Questions
Which APIs make a route dynamic?
Does reading a cookie always make the whole page dynamic?
Conclusion
Dynamic APIs read data that only exists when a real request arrives. They opt a route out of static rendering in the old model, but under Cache Components they only make the boundary around the read stream.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.