errors/blocking-prerender-dynamic.mdx
During prerendering, a fetch() request, database call, await connection(), or other asynchronous IO ran outside of <Suspense>. With Cache Components enabled, Next.js can't prerender the part of the tree that depends on this data, so navigations to this route block instead of being instant.
Request-bound reads (cookies(), headers(), params, searchParams) have different fixes. See Next.js encountered runtime data during prerendering.
This error can also appear during a client-side navigation when the data access sits inside a <Suspense> boundary from a parent layout but that boundary is too high. It wraps the entire segment instead of only the dynamic part, so the navigation still blocks. Push the boundary closer to the data access so the rest of the segment stays in the static shell. See Choosing where to place the boundary.
Choose this fix when the data must be fresh on every request. A <Suspense> boundary lets the static shell ship instantly while the dynamic region streams in once the data resolves.
Keep the component that performs the data access intact and add a <Suspense> boundary around its usage in the parent.
import { Suspense } from 'react'
import { TransactionList } from './transaction-list'
import { TransactionSkeleton } from './transaction-skeleton'
export default function Page() {
return (
<Suspense fallback={<TransactionSkeleton />}>
<TransactionList />
</Suspense>
)
}
Learn more: Streaming.
When the page reads data at the top and forwards it down, move the read into the component that consumes it. The parent stays static and the boundary wraps only the part that needs the data.
import { Suspense } from 'react'
export default function Page() {
return (
<main>
<DashboardHeader />
<Suspense fallback={<TransactionSkeleton />}>
<LatestTransactions />
</Suspense>
</main>
)
}
export async function LatestTransactions() {
const transactions = await db.transactions.findMany({ take: 20 })
return <TransactionList transactions={transactions} />
}
Learn more: Streaming.
When several siblings each fetch independently, give each one its own boundary so the streamed regions arrive in parallel instead of waiting on the slowest one.
import { Suspense } from 'react'
export default function Page() {
return (
<main>
<Suspense fallback={<MetricsSkeleton />}>
<Metrics />
</Suspense>
<Suspense fallback={<TransactionSkeleton />}>
<LatestTransactions />
</Suspense>
</main>
)
}
Learn more: Streaming.
loading.js for the whole segmentWhen the entire page depends on the failing data access and there's nothing static to render above it, a loading.js file in the segment is the shorthand. Next.js wraps {children} of the layout in <Suspense> automatically.
export default function Loading() {
return <DashboardSkeleton />
}
Good to know: A
loading.jsfile wraps the segment's{children}in one Suspense boundary. Parent layouts above it still prerender, but everything inside the segment sits behind the fallback. If page-level content could be prerendered (a static intro, a known title), use explicit<Suspense>boundaries insidepage.jsaround only the dynamic parts.
Learn more: loading.js and instant loading states.
The shell ships immediately, but the user sees a loading state for the streamed region on every request. Design the fallback so it approximates the final layout. A generic spinner causes a layout shift when content arrives. See minimizing layout shift.
The location of the boundary controls what the user sees during the navigation:
A useful rule: push the boundary as low as possible while keeping the fallback meaningful. The cached content above the boundary becomes part of the static shell on navigation. Wrapping individual pieces or wrapping the whole page in one boundary stream the same way, but a lower boundary keeps more prerendered content visible during the navigation. See Maximizing the static shell for the canonical pattern.
Math.random() or Date.now() inside the fallback raises a separate Cache Components error during prerendering.{children} through in the fallback. Child pages may include dynamic reads (for example, /_not-found calling cookies() or an uncached fetch()) that propagate into what should be a static fallback. Render a placeholder that doesn't include {children}./_not-found and you don't have a not-found.tsx file, the read is in the root layout. /_not-found is a real prerendered route that inherits the root layout, so an uncached read there fails on the synthetic route too. Run next build --debug-prerender to confirm the originating file, and fix it at the layout, not by adding a not-found.tsx.<html lang>, <html dir>, <html data-theme>) can't be wrapped in <Suspense>. You can't suspend the document root, and a boundary inside <html> still leaves the attribute itself server-cookie-dependent. Move the read to a pre-paint client script per Preventing flash before hydration and add suppressHydrationWarning on <html> so React doesn't flag the script's mutation as a mismatch.Choose this fix when the data does not need to be regenerated on every request. Move the call into a function and add the use cache directive as the first statement of the function body. The function still runs the underlying query, but Next.js caches the result for the configured lifetime and the surrounding route becomes prerenderable.
This fix does not apply to connection(). The whole point of connection() is to opt into per-request rendering for the wrapped subtree, so caching it would defeat the purpose. Use Wrap in or move into Suspense instead.
Move the fetch() or database call into its own function and mark that function with use cache. Arguments to the function and closed-over variables become part of the cache key, so prefer passing the values you depend on as arguments to make the contract explicit.
async function getRecentTransactions(limit) {
'use cache'
return db.transactions.findMany({
orderBy: { createdAt: 'desc' },
take: limit,
})
}
export default async function Page() {
const transactions = await getRecentTransactions(10)
return <TransactionList transactions={transactions} />
}
Learn more: Fetching Data.
When the component does nothing but read data and render it, mark the component itself with use cache. Next.js caches the rendered JSX, which is cheaper to reuse than recomputing it from the cached data.
export async function TransactionList({ limit }) {
'use cache'
const transactions = await db.transactions.findMany({ take: limit })
return (
<ul>
{transactions.map((transaction) => (
<li key={transaction.id}>{transaction.description}</li>
))}
</ul>
)
}
Learn more: Caching with use cache.
Choose this when you want control over when the cached value is refreshed. Tag the entry with cacheTag and invalidate it on demand: call updateTag from a Server Action when the user performed the mutation and should see fresh data on the next request, or revalidateTag from a route handler, cron, admin tool, or incoming webhook for stale-while-revalidate refreshes. Tags add an on-demand invalidation path on top of the cacheLife expiration window. The two are independent.
import { cacheTag } from 'next/cache'
async function getRecentTransactions() {
'use cache'
cacheTag('dashboard-transactions')
return db.transactions.findMany({ take: 10 })
}
Learn more: How revalidation works.
cacheLife profileWhen the data has a natural shelf-life (hourly metrics, daily aggregates), pick a cacheLife profile that matches. Without a profile, Next.js uses the project default.
import { cacheLife } from 'next/cache'
async function getDashboard() {
'use cache'
cacheLife('hours')
return db.metrics.summary()
}
Learn more: How to configure cache lifetimes.
Freshness becomes a property of the cache configuration, not the data source. The cached response is reused until cacheLife revalidates or expires, or until cacheTag is invalidated. Plan invalidations alongside the code that mutates the data. Call updateTag from a Server Action when the user performed the mutation and should see fresh data on the next request, or revalidateTag from a route handler, cron, or webhook for stale-while-revalidate refreshes.
"use cache" directive runs on the server. It can't wrap a function that uses runtime APIs such as cookies() or headers(). Read those outside the cached scope and pass the values as arguments, or use "use cache: private".cacheLife may be too short to prerender. See Short-lived caches."use cache: remote" instead. It trades a network roundtrip for a single cache shared by all servers."use cache" accepts a cacheLife profile. A short profile (such as "seconds" or "minutes") whose revalidate is shorter than the prerender's effective lifetime prevents the value from being included in the prerender. The segment becomes a dynamic hole instead. The cache entry still helps the Client Cache and protects upstream APIs, but the page falls back to streaming.
To keep the page prerendered, use a profile with a longer revalidate window such as "default" (15 minutes), "hours", or "days". If a short profile is intentional, treat the value as dynamic and use Wrap in or move into Suspense instead.
Choose this fix when the route renders per-request and there's no useful static shell. Setting instant to false exempts the segment from instant-navigation validation. The page renders on every request and the navigation blocks until that render completes.
Add the export to the page that triggered the error. Only that route blocks.
export const instant = false
export default async function Page() {
const data = await getDashboard()
return <Dashboard data={data} />
}
Learn more: Ensuring instant navigations.
When the shared layout itself can't ship instantly (it reads runtime data or uncached data of its own), set instant to false on the layout. This allows that layout segment to block while descendant segments remain independently validated.
export const instant = false
export default function DashboardLayout({ children }) {
return <DashboardShell>{children}</DashboardShell>
}
Learn more: Route segment instant config.
Use either pattern when:
Don't use this to dismiss the error. Choose Cache the component or data or Wrap in or move into Suspense when either is feasible.
Navigations to this route are not instant. The user waits for the full server render before any HTML arrives. Use this only when that latency is necessary for the route to function.
instant to false opts only the segment that exports it out. Descendant segments are still validated by the global default.After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in. A <Suspense> boundary placed around the whole page body can pass validation with an empty shell, which defeats the point of an instant navigation.
In next dev, the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default next build output is more abbreviated. Run next build --debug-prerender for full user-frame stack traces and next build --debug-build-paths /dashboard /settings to iterate on specific routes.
Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.
export const instant = false to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.experimental.instantInsights.validationLevel to 'manual-warning' in next.config. This limits validation to segments that explicitly export instant.See Ensuring instant navigations for the full model.
generateMetadata()generateMetadata()generateViewport()generateViewport()Math.random() while prerenderingMath.random() in a Client ComponentDate.now() while prerenderingDate.now() in a Client Component