io()

Marks that a synchronous non-deterministic read follows, keeping it out of the static shell without blocking prefetch.

Signature
function io(): Promise<void>
  • Added in Next.js v16.3.0

Return value

Promise<void>A Promise<void>. With Cache Components enabled, awaiting it stops prerendering so the code that follows is excluded from the prerender output; in every other context it resolves immediately.

Changelog

  • PHP v16.3.0io added.

With Cache Components on, Next.js needs you to decide what happens to a synchronous value like new Date() or Math.random(): capture it once into the static shell, or produce it fresh per request. Wrapping it in "use cache" chooses the first. await io() chooses the second — it suspends during a prerender, so everything after it lands outside the shell.

When to reach for it

Put it immediately before a synchronous, non-deterministic read: the clock, a random number, crypto.randomUUID(), or a synchronous driver such as node:sqlite. In a Client Component the same applies during SSR, where you call it as use(io()).

When you do not need it

If the component already calls cookies() or headers(), that call is the suspension point. If the value comes from an awaited fetch or async query inside <Suspense>, the await is the suspension point.

What trips people up

The suspension needs a <Suspense> boundary above it, or there is no fallback to put in the shell. Inside a "use cache" scope io() is a no-op — the value gets captured, which may not be what you meant.

Example

Example · typescript
import { Suspense } from 'react'
import { io } from 'next/cache'

export default function Page() {
  return (
    <Suspense fallback={<p>Loading...</p>}>
      <CurrentTime />
    </Suspense>
  )
}

async function CurrentTime() {
  await io()
  return <p>{new Date().toISOString()}</p>
}

When to use it

  • Render a live timestamp that must not be frozen into the prerendered HTML
  • Keep a Math.random() experiment bucket out of the static shell while staying prefetchable
  • Read node:sqlite synchronously per request under Cache Components
  • Migrate a connection() call that was needlessly blocking prefetches

Frequently asked questions

What is the difference between io() and connection()?

io() suspends like any ordinary async function, so the code after it can still be wrapped in "use cache", prefetched and cached on the client, while connection() stays suspended until a full user navigation reaches the server and therefore blocks prefetches. Prefer io(); reach for connection() only when you must wait for a real user request.

Do I need io() without Cache Components?

No. Outside Cache Components — including the Pages Router, real requests, cache scopes and the browser — io() resolves immediately and has no effect. It exists to give the prerender a suspension point it would otherwise not have.

Why does my page error when I call io()?

Usually because there is no <Suspense> boundary above the component that awaits it. The suspension needs a fallback to place in the static shell; without one, the prerender has nothing to emit for that subtree.

More examples

In a Client Component with use()

Client Components are still prerendered on the server during SSR, so Date.now() would otherwise be captured into the static shell and served stale to every visitor. use(io()) is the client-side equivalent of the await: it suspends the component during that server pass and lets it read the clock for real on each request.

Example · typescript
'use client'

import { use } from 'react'
import { io } from 'next/cache'

export function CurrentTime() {
  use(io())
  return <div>{Date.now()}</div>
}

Capturing the value instead, with "use cache"

This shows the opposite choice. Inside a caching directive scope io() does nothing, the timestamp is captured into the static shell, and no <Suspense> boundary is required — every visitor sees the same value until the cache entry is revalidated. Reaching for io() and then wrapping the caller in "use cache" by accident is the usual way people end up with a frozen clock.

Example · typescript
import { io } from 'next/cache'

async function BuiltAt() {
  'use cache'
  // io() is a no-op inside a cache scope: the value is captured
  await io()
  return <p>Built at {new Date().toISOString()}</p>
}

export default function Page() {
  return <BuiltAt />
}

Not needed when a request API is already present

Once cookies() has suspended the component, the whole component is already outside the static shell, so the new Date() after it is safe. Adding io() here would be harmless but redundant — the rule is one suspension point per dynamic region, not one per non-deterministic read.

Example · typescript
import { Suspense } from 'react'
import { cookies } from 'next/headers'

export default function Page() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <Greeting />
    </Suspense>
  )
}

async function Greeting() {
  // No io() needed — cookies() is itself the suspension point
  const name = (await cookies()).get('name')?.value ?? 'there'
  const greetedAt = new Date().toISOString()

  return (
    <p>
      Hello {name}, at {greetedAt}
    </p>
  )
}

Discussion

  • Be the first to comment on this lesson.