Next.js encountered uncached data in generateMetadata()
This Insight is part of the Instant Navigations feature introduced in Next.js 16.3. If you're new to it, start with the Ensuring instant navigations guide for an overview of what instant navigations are and how Next.js validates them, then come back here for the specific fix.
During prerendering, generateMetadata() performed an uncached data access (fetch(), database call, await connection()). With Cache Components enabled, Next.js expects metadata to be prerenderable when the rest of the route is. This route's metadata is blocked, but the rest of its content can be prerendered.
Request-bound reads (cookies(), headers(), params, searchParams) in generateMetadata() have different fixes. See Next.js encountered runtime data in generateMetadata().
The viewport equivalent is handled at Uncached data in generateViewport().
For errors in the page body rather than metadata, see Next.js encountered uncached data during prerendering.
Ways to fix this
Cache the metadata
Choose this fix when the metadata comes from an external source (CMS, database) but doesn't need to change on every request. Add the use cache directive as the first statement inside generateMetadata(). Next.js caches the returned metadata object and includes it in the prerender.
This fix does not apply to connection(). The point of connection() is to opt into per-request rendering, so caching it would defeat the purpose. Use Mark the route as dynamic instead.
Patterns
Add use cache to generateMetadata
Mark the function as cacheable. The metadata is evaluated once per cache window and reused.
import { cms } from './cms'
export async function generateMetadata({ params }) {
'use cache'
const { slug } = await params
const { title } = await cms.getPageData(slug)
return { title }
}
async function getPageText(slug) {
'use cache'
const { text } = await cms.getPageData(slug)
return text
}
export default async function Page({ params }) {
const { slug } = await params
const text = await getPageText(slug)
return <article>{text}</article>
}Learn more: Caching with use cache.
Tag the metadata for invalidation
When you publish new content and want the metadata to refresh, tag the entry with cacheTag. Invalidate from a Server Action with updateTag (read-your-own-writes: the next request waits for fresh data) or from a Route Handler with revalidateTag.
import { cacheTag } from 'next/cache'
import { cms } from './cms'
export async function generateMetadata({ params }) {
'use cache'
const { slug } = await params
cacheTag(`meta-${slug}`)
const { title } = await cms.getPageData(slug)
return { title }
}Learn more: How revalidation works.
Trade-off
Freshness depends on the cache configuration. The metadata stays the same until cacheLife revalidates or expires, or until cacheTag is invalidated. Plan invalidations alongside the code that mutates the content.
Gotchas
use cachecan't be combined withcookies()orheaders()in the same scope. Inside a cached function, you can't call request-bound APIs. If the metadata needs a request-bound value (a session token to call a protected API), read it outside the cached scope and pass it as an argument, or use Mark the route as dynamic instead.- If the metadata function reads
params, the params become part of the cache key automatically. Each unique param set gets its own cached metadata entry. - A short
cacheLife(a profile whoserevalidateis shorter than the prerender's effective lifetime) prevents the metadata from being included in the prerender. The route becomes partially dynamic. Use a longer profile if you want the metadata included in the static shell.
Mark the route as dynamic
Choose this fix when the rest of the page is fully static and you want the metadata to remain dynamic. Add a small component that calls await connection(), render null from it, and wrap it in <Suspense>.
This error fires specifically because the metadata is the only dynamic part of an otherwise fully prerenderable route. Adding a dynamic marker is an explicit signal to Next.js that the page has intentional dynamic content streamed alongside the static shell, so the dynamic metadata is allowed.
Patterns
Add a dynamic marker component
Create a small component that calls connection() and renders nothing, wrapped in <Suspense>. The page content remains prerenderable and only the marker is excluded from the prerender.
import { Suspense } from 'react'
import { connection } from 'next/server'
export async function generateMetadata() {
const response = await fetch('https://api.example.com/meta')
const { title } = await response.json()
return { title }
}
async function DynamicMarker() {
await connection()
return null
}
export default function Page() {
return (
<>
<article>This article is completely static</article>
<Suspense>
<DynamicMarker />
</Suspense>
</>
)
}Learn more: connection.
Trade-off
The metadata and the dynamic marker run on every request, so the route cannot be fully static. The rest of the page content still prerenders, and only the metadata blocks the initial paint.
Gotchas
- The
DynamicMarkermust be wrapped in<Suspense>. Without the boundary, the dynamic marker propagates up and the entire page is treated as blocking, surfacing the same blocking-route error this fix is meant to address. - This pattern is intentionally verbose. If you find yourself adding a dynamic marker, reconsider whether the metadata can be cached instead. Most metadata doesn't need to be per-request.
- If the page already has a genuinely dynamic component (one that reads
cookies()or uncached data inside a<Suspense>boundary), you won't see this error. The page is already partially dynamic. - Framework-synthesized routes (
/_not-found,/_global-error) inherit the root layout'sgenerateMetadataand must be statically prerendered. The dynamic marker doesn't help here, because these routes don't have a page body where you can place a Suspense'd marker. If your root layout'sgenerateMetadatadepends on uncached data, Cache the metadata instead, or move toglobal-not-found.js, which bypasses the root layout entirely and avoids inheriting itsgenerateMetadata.
Verifying the fix
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.
Don't want this validation?
Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.
- One segment: add
export const instant = falseto the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations. - Entire app: set
experimental.instantInsights.validationLevelto'manual-warning'innext.config. This limits validation to segments that explicitly exportinstant.
See Ensuring instant navigations for the full model.
Related Insights
- Runtime data during prerendering
- Uncached data during prerendering
- URL data in a Client Component outside of Suspense
- Runtime data in
generateMetadata() - Runtime data in
generateViewport() - Uncached data in
generateViewport() Math.random()while prerenderingMath.random()in a Client ComponentDate.now()while prerenderingDate.now()in a Client Component- Crypto APIs while prerendering
- Crypto APIs in a Client Component
- Dynamic data during prefetching
- URL data outside of Suspense
- Unrendered segment
Was this helpful?