errors/blocking-prerender-crypto.mdx
During prerendering, a Server Component called a synchronous Web Crypto or Node crypto API that produces a random value (crypto.randomUUID(), crypto.getRandomValues(), crypto.randomBytes(), crypto.generateKeyPairSync()) outside of <Suspense>. With Cache Components enabled, Next.js can't bake an unpredictable value into the prerendered HTML. The value at build time will differ from the value at runtime, so you need to choose: cache the generated value so it's stable, defer the call behind a <Suspense> boundary so it runs per-request, or move it to the client.
Other unpredictable APIs (Math.random(), Date.now()) have parallel error pages: see Math.random() and Date.now(). The Client Component case is handled at Crypto APIs in a Client Component.
Choose this fix when each request needs a fresh token: a session ID, an OAuth state, a single-use nonce, a CSRF token. Add await connection() before the call to tell Next.js the surrounding component is request-bound. The component is excluded from the prerender and streamed in from the nearest <Suspense> boundary on each request.
await connection() before the crypto callCall connection() before the crypto API. Everything after the await is request-time. Wrap the component in <Suspense> so the surrounding shell stays prerendered and only the dynamic part streams in.
Push the <Suspense> boundary as close to the crypto call as possible. If the parent has cached content, isolate the crypto read in its own component so only that piece falls behind the boundary.
import { Suspense } from 'react'
export default function Page() {
return (
<DashboardShell>
<Suspense fallback={null}>
<CsrfToken />
</Suspense>
<CachedStats />
</DashboardShell>
)
}
import { connection } from 'next/server'
export async function CsrfToken() {
await connection()
return <input type="hidden" name="csrf" value={crypto.randomUUID()} />
}
await io()Use io() from next/cache to keep the read out of the static shell. Unlike connection(), io() doesn't block prefetches and works inside "use cache" scopes and Client Components.
import { io } from 'next/cache'
export async function CsrfToken() {
await io()
return <input type="hidden" name="csrf" value={crypto.randomUUID()} />
}
Learn more: connection, io, Streaming.
When an async equivalent of the API exists, prefer it. Async crypto operations integrate with <Suspense> naturally and don't need await connection(): the await already tells Next.js the surrounding scope is request-time.
import { randomBytes } from 'node:crypto'
import { promisify } from 'node:util'
import { Suspense } from 'react'
const randomBytesAsync = promisify(randomBytes)
export default async function Page() {
return (
<Suspense fallback={<TokenSkeleton />}>
<TokenDisplay />
</Suspense>
)
}
async function TokenDisplay() {
const buf = await randomBytesAsync(32)
return <code>{buf.toString('hex')}</code>
}
Learn more: Node crypto API.
The route renders on every request. The shell still ships instantly because of the <Suspense> boundary, but the dynamic region waits on the server render before it paints. When the fallback is visible UI, design it to approximate the final layout so it doesn't cause a layout shift when the content arrives. See minimizing layout shift.
<Suspense> fallbacks, loading.js, error.js, not-found.js, and global-error.js. Calling a crypto API in any of them raises this same error.crypto.subtle.digest(), crypto.generateKeyPair()) integrate with <Suspense> naturally and don't trip the error.Choose this fix when the generated value is a key into another cached operation. The classic case is a service that requires a token: generate the token once, cache it, and let it serve as the cache key for downstream lookups. The user-visible value never changes across visitors, which is fine because the user never sees the token directly.
Wrap both the token generation and the call that consumes it in the same use cache function.
async function getCachedData() {
'use cache'
const token = crypto.randomUUID()
return db.query(token /* … */)
}
export default async function Page() {
const data = await getCachedData()
return <View data={data} />
}
Learn more: Caching with use cache.
When you want to rotate the token on a schedule or in response to an event, 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 (stale-while-revalidate).
import { cacheTag } from 'next/cache'
async function getApiToken() {
'use cache'
cacheTag('api-token')
return crypto.randomBytes(32).toString('hex')
}
Learn more: How revalidation works.
Every visitor in the cache window uses the same generated value. That's the right answer for upstream cache keys and signing keys you control, and the wrong answer for per-user identity (sessions, CSRF, nonces).
use cache can't combine with cookies() or headers() in the same scope, so you can't key the cached value by user identity from inside the cached function.cacheLife may be too short to prerender. See Short-lived caches.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 Generate on every request instead.
Choose this fix when the generated value belongs to the client experience. A client-only correlation ID for telemetry, a draft-state key in localStorage, a UI nonce for a confirmation modal. Move the component into a Client Component so the value is produced after hydration.
Add use client and call the crypto API inside the component.
'use client'
import { startTransition, useEffect, useState } from 'react'
export function DraftKey() {
const [key, setKey] = useState(null)
useEffect(() => {
// Wrap in startTransition so that if any component below suspends
// during this update, React keeps the existing UI visible instead
// of flashing the nearest outer <Suspense> fallback.
startTransition(() => {
setKey(crypto.randomUUID())
})
}, [])
return <input type="hidden" value={key ?? ''} />
}
Learn more: Client Components.
The first paint shows the SSR fallback (often null), and the value appears only after the browser hydrates the component. That's fine for client-only state but wrong for tokens that have to be in the prerendered HTML. See Preventing flash before hydration for techniques that eliminate the flash.
<Suspense> and effect-based recipes.crypto.randomBytes, crypto.generateKeyPairSync) are not available on the client.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 = false doesn't clear this errorThis error fires from the prerender, not from instant-navigation validation. crypto.randomUUID() and related APIs return a different value on every call, so the prerender can't bake them into a static shell regardless of the segment's instant config or experimental.instantInsights.validationLevel. Use one of the fixes above.
generateMetadata()generateMetadata()generateViewport()generateViewport()Math.random() while prerenderingMath.random() in a Client ComponentDate.now() while prerenderingDate.now() in a Client Component