Things that opt a Next.js route out of the cache
Note: This guide assumes you are not using Cache Components which was introduced in Next.js 16. For the new explicit opt-in model, see the Cache Components series.
A Next.js route can be statically rendered at build time, with the HTML and RSC payload stored in the Full Route Cache and reused for every visitor. Static rendering only works when the output is the same for everyone, because the result is computed once and served many times. That's the rendering strategy you want to use for certain page types like landing pages, blog posts, product listings, or documentation.
But when your page needs request-time information that can only be known at the moment a user visits, static rendering is no longer possible. Information like the user's cookies or incoming request headers has to be resolved at request time. Next.js handles this by flipping the entire route to dynamic rendering as soon as you use any API that depends on the request.
Below is the list of APIs and patterns that trigger this switch, with a short explanation of each.
Rendering strategy: static vs. dynamic
Every Next.js route has one of two rendering strategies.
Static rendering happens at build time or during background revalidation. The HTML and the React Server Component payload are generated once and served to every visitor from the Full Route Cache.
Dynamic rendering happens at request time. Each visit triggers a fresh render on the server because the output depends on something only the incoming request knows.
Use any of the APIs below and the whole route becomes dynamic.
cookies()
Cookies only exist in the context of an incoming request. Calling cookies() from next/headers forces the route into dynamic rendering.
import { cookies } from 'next/headers'
export default async function Page() {
const sessionId = (await cookies()).get('cart-id')?.value
}
Use this for per-request data like authentication state, user preferences, or shopping carts.
headers()
Same story as cookies. Calling headers() from next/headers forces dynamic rendering because the incoming headers are part of the request.
import { headers } from 'next/headers'
export default async function Page() {
const authorization = (await headers()).get('Authorization')
}
Common cases: reading Authorization for server-side auth, geo headers injected by a CDN, or Accept-Language for localization.
searchParams prop
searchParams is a request-time API whose values cannot be known ahead of time. Any page component that reads searchParams will be rendered dynamically, because the query string changes from visit to visit (and can be anything on manual inputs) and there's no way to precompute every possible combination at build time.
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const { q } = await searchParams
}
If you only need the parameters for client-side display, use the useSearchParams React hook in a Client Component instead.
connection()
Introduced in Next.js 16, connection() is the explicit declaration for components that should render at runtime without actually reading any request-time API. From the Next.js docs:
The
connection()function allows you to indicate rendering should wait for an incoming user request before continuing. It's useful when a component doesn't use Request-time APIs, but you want it to be rendered at runtime and not prerendered at build time.
An example case is a component that calls Math.random() or new Date() and needs that value evaluated per request rather than at build time.
import { connection } from 'next/server'
export default async function Page() {
await connection()
return <p>Rendered at {new Date().toISOString()}</p>
}
fetch with { cache: 'no-store' }
Next.js extends the native fetch API to integrate with the Data Cache. Setting { cache: 'no-store' } bypasses the cache, so the fetch hits the data source on every call.
const res = await fetch('https://api.example.com/stock-price', {
cache: 'no-store',
})
Using no-store inside a Server Component makes the surrounding route dynamic. Use it for data that genuinely changes on every request, like live prices.
Missing generateStaticParams on dynamic routes
Dynamic route segments like [slug] or [id] need to know which parameter values exist at build time in order to generate static HTML for each one. You declare those values with generateStaticParams.
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({ slug: post.slug }))
}
Without generateStaticParams, Next.js falls back to dynamic rendering for the route. Returning an empty array is also a valid option if you want routes to be generated incrementally on first request rather than at build time.
Wrapping up
When a route in your production build shows up as dynamic and you can't figure out why, the cause is one of the items above. The call doesn't necessarily have to be in your own code, it can come from a library or helper function several levels deep. Start tracing from the route entry point and follow the imports until you find the request-time call.
For new projects on Next.js 16, the Cache Components series covers the new opt-in caching model in detail, including how revalidation works and how to migrate from the previous caching model.