Skip to content

Next.js encountered the unstable value Date.now() while prerendering

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, a Server Component called Date.now(), Date(), or new Date() outside of <Suspense>. With Cache Components enabled, Next.js can't bake "now" into the prerendered HTML. The timestamp at build time will be stale at runtime, so you need to choose: cache the value (treat it as "now-ish" with a tolerated drift), defer the read behind a <Suspense> boundary so it runs per-request, or move it to the client.

Other unpredictable APIs (Math.random(), crypto.randomUUID()) have parallel error pages: see Math.random() and crypto APIs. The Client Component case is handled at Date.now() in a Client Component.

Ways to fix this

Generate on every request

Choose this fix when the user needs to see the current time. A "last updated at" banner, a server-issued timestamp on a transaction, a "happy new year" banner that flips at midnight. 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.

Patterns

Use await connection() before the timestamp read

Call connection() before the Date.now() or new Date() call. 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 timestamp read as possible. If the parent has cached content (metrics, headers, navigation), isolate the timestamp in its own component so only that piece falls behind the boundary.

app/dashboard/page.js
import { Suspense } from 'react'
 
export default function Page() {
  return (
    <DashboardShell>
      <Suspense fallback={<UpdatedAtSkeleton />}>
        <UpdatedAt />
      </Suspense>
      <CachedMetrics />
    </DashboardShell>
  )
}
app/dashboard/updated-at.js
import { connection } from 'next/server'
 
export async function UpdatedAt() {
  await connection()
  return <small>Updated at {new Date().toLocaleString()}</small>
}

Alternative: 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.

app/dashboard/updated-at.js
import { io } from 'next/cache'
 
export async function UpdatedAt() {
  await io()
  return <small>Updated at {new Date().toLocaleString()}</small>
}

Learn more: io, Streaming.

Trade-off

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. Design the fallback so it approximates the final layout. A generic spinner or empty box causes a layout shift when the content arrives. See minimizing layout shift.

Gotchas

Cache the timestamp

Choose this fix when a stale timestamp is acceptable for the cache window. A "last refreshed" footer on a daily report, an "as of" banner on an hourly chart. Move the Date.now() call into a function with use cache. Next.js evaluates the function once per cache key and reuses the result.

Patterns

Cache the producer function

Wrap the timestamp read in a function with use cache. The returned value is part of the cache entry, so every consumer sees the same timestamp until the cache is invalidated.

app/page.js
async function getRenderedAt() {
  'use cache'
  return Date.now()
}
 
export default async function Page() {
  const renderedAt = await getRenderedAt()
  return <ReportFooter renderedAt={renderedAt} />
}

Learn more: Caching with use cache.

Cache the timestamp alongside the data it relates to

When the timestamp captures "when this cached content was produced" (a "last refresh" footer, a generation timestamp), include the Date.now() read inside the same cached function that produces the data. The timestamp and the data are part of the same cache entry, so they refresh together.

app/dashboard.js
async function InformationTable() {
  'use cache'
  const data = await fetch('https://api.example.com/info')
  return (
    <>
      <table>{renderData(await data.json())}</table>
      <small>Last refresh: {new Date().toString()}</small>
    </>
  )
}

For values like the current year used in a copyright footer, the cached read effectively never changes (until the next year), so it can live in a cacheLife('max') entry.

app/layout.js
import { cacheLife } from 'next/cache'
 
async function getCurrentYear() {
  'use cache'
  cacheLife('max')
  return new Date().getFullYear()
}
 
export default async function Layout({ children }) {
  return (
    <>
      <main>{children}</main>
      <footer>Copyright {await getCurrentYear()}</footer>
    </>
  )
}

Set the rotation window with cacheLife

When you want the timestamp to refresh on a schedule, set a cacheLife profile.

app/page.js
import { cacheLife } from 'next/cache'
 
async function getHourlyTimestamp() {
  'use cache'
  cacheLife('hours')
  return Date.now()
}

Learn more: How to configure cache lifetimes.

Trade-off

Every visitor in the cache window sees the same timestamp. That's fine for "as of" labels on cached data, wrong for "right now" labels.

Gotchas

  • A cached timestamp is the time of the most recent cache miss, not the time of the current visit. If users expect the displayed time to match their visit, use Generate on every request instead.
  • If you cache a function and still see this error, the cacheLife may be too short to prerender. See Short-lived caches.

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.

Render on the client

Choose this fix when the timestamp belongs to the user's local clock or needs to update while they're on the page. A relative time label that ticks ("3 seconds ago"), a localized time display, a stopwatch. Move the component into a Client Component so the value is produced after hydration.

Patterns

Move the component to the client

Add use client and compute the timestamp inside the component.

app/now.js
'use client'
 
import { startTransition, useEffect, useState } from 'react'
 
export function Now() {
  const [now, setNow] = 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(() => {
      setNow(Date.now())
    })
    const id = setInterval(() => {
      startTransition(() => {
        setNow(Date.now())
      })
    }, 1000)
    return () => clearInterval(id)
  }, [])
  return <time>{now ? new Date(now).toLocaleString() : '…'}</time>
}

Learn more: Client Components.

Trade-off

The first paint shows the SSR fallback (often null or an em-dash), and the timestamp appears only after the browser hydrates the component. That's fine for time-of-day displays but wrong for timestamps that have to be in the prerendered HTML. See Preventing flash before hydration for techniques that eliminate the flash.

Gotchas

For telemetry, use a timing API

Choose this fix when the timestamp isn't user-visible at all. Logging, performance instrumentation, span correlation: all measurements that need a clock but don't render anything. Switch to performance.now(), a high-resolution monotonic timer that doesn't carry the same semantic ("the current wall-clock time") that prevents prerendering.

Patterns

Replace Date.now() with performance.now()

Drop-in replacement for any elapsed-time calculation.

app/page.js
export default async function Page() {
  const start = performance.now()
  const data = await computeReport()
  const elapsedMs = performance.now() - start
  console.log(`computeReport took ${elapsedMs}ms`)
  return <Report data={data} />
}

Learn more: performance.now().

Trade-off

performance.now() returns a high-resolution timestamp relative to time origin, not a wall-clock time. It's the wrong tool if you actually need to know "what is today's date". Use it only for durations.

Gotchas

  • performance.now() values from the server can't be compared to values from the browser. Each environment has its own time origin.
  • Don't pass a performance.now() value into the rendered output or into a cached function. The high-resolution timestamp is non-deterministic and shouldn't influence what's prerendered.
  • For absolute time in an observability tool, use performance.timeOrigin + performance.now() to get a wall-clock timestamp without tripping this error.

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.

Why instant = false doesn't clear this error

This error fires from the prerender, not from instant-navigation validation. new Date() and Date.now() return a different value on every render, 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.

Was this helpful?

supported.