Dynamic APIs in Next.js: How `cookies`, `headers`, and `searchParams` Opt You Out of Static Rendering

How the dynamic APIs cookies(), headers(), and searchParams turn a route dynamic, and how Cache Components change that into per-boundary streaming.

8 min read

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.
  • searchParams reads 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.tsxApp.tsx
// 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.tsxApp.tsx
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Which APIs make a route dynamic?

The request-time APIs: cookies(), headers(), searchParams, and draftMode(). Dynamic route params do the same when they are not prerendered by generateStaticParams.

Does reading a cookie always make the whole page dynamic?

Under the pre-Cache-Components model, yes. With Cache Components enabled, only the Suspense boundary around the read streams, while the rest of the page stays in the static shell.

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.