The connection() function tells Next.js that rendering should wait for an incoming request before continuing. It is the deliberate switch to dynamic rendering when a component does not read any request-time API but still needs to produce different output per request.
It takes no arguments and returns a void Promise that you do not consume. Import it from next/server, and it runs on the server only. On the client the function is meaningless, because Client Components already render in the browser.
The examples here assume Cache Components, enabled with cacheComponents: true in your Next config file. That matters, because under Cache Components a suspended read has to sit inside a Suspense boundary or the build fails.
Per-request randomness
The clearest case is a value that changes every call. Without a suspension point, a component that only calls Math.random() counts as predictable work, so it is prerendered once and the number freezes for everyone.
// app/page.tsx
import { connection } from 'next/server'
import { Suspense } from 'react'
async function LuckyNumber() {
await connection()
return <span>{Math.random()}</span>
}
export default function Page() {
return (
<Suspense fallback={<span>Rolling...</span>}>
<LuckyNumber />
</Suspense>
)
}The await stops prerendering at that line, so the fallback ships in the static shell and the number is generated per request. Date.now() and crypto.randomUUID() behave the same way, which is why a clock or request ID otherwise shows a build-time value to every visitor.
Synchronous database reads
Some database drivers read synchronously, so Next.js cannot see the read as an async boundary and would prerender the result. A driver like better-sqlite3 completes its query during the build, which is wrong when the data changes per request. Awaiting connection() first forces the query to run per request.
// app/lib/data.ts
import { connection } from 'next/server'
import Database from 'better-sqlite3'
const db = new Database('app.db')
export async function getVisitorCount() {
await connection()
return db.prepare('SELECT value FROM counters WHERE name = ?').get('visitors')
}Any component that calls getVisitorCount() is now excluded from prerendering. The synchronous query runs when a visitor actually arrives, so the count stays fresh. The same applies to any synchronous read whose result should not be frozen at build time.
connection() versus io()
connection() replaces the deprecated unstable_noStore. Next.js 16.3 added io(), imported from next/cache, which also keeps the code after it out of the static shell. The two are not interchangeable.
| Function | Suspends until | Can be cached or prefetched |
|---|---|---|
| connection() | A real user request reaches the server | No, it blocks prefetches |
| io() | Like any async call | Yes |
That difference is the whole decision. Because connection() waits for an actual navigation, it also blocks the prefetch that would otherwise warm the route, so a link to that page cannot be prepared ahead of the click. The official guidance is to prefer io() and reach for connection() only when rendering genuinely must wait for a real request.
// app/clock/page.tsx
import { io } from 'next/cache'
import { Suspense } from 'react'
async function CurrentTime() {
await io()
return <p>{new Date().toISOString()}</p>
}
export default function Page() {
return (
<Suspense fallback={<p>Loading...</p>}>
<CurrentTime />
</Suspense>
)
}This renders the same per-request timestamp, but the boundary can still be prefetched, so a navigation into the page lands with the surrounding UI already in place. Inside a use cache scope it does nothing at all, because the value is being captured into the shell on purpose.
Common mistakes
Four mistakes show up repeatedly.
- Reaching for connection() when a request-time API such as
cookies()is already in the component. The request-time API is itself the suspension point, so connection() adds nothing. - Using connection() where io() fits, which silently gives up prefetching for that boundary.
- Calling connection() from a Client Component, where it has no meaning.
- Awaiting connection() outside a Suspense boundary under Cache Components, which fails the build instead of making the route dynamic.
For the request-time APIs that make a route dynamic on their own, see the dynamic APIs article. For diagnosing an unexpected dynamic route, see why a route became dynamic.
Rune AI
Key Insights
⚠ This article has a formatting issue and may not display correctly.
Our team has been notified. The content is shown as plain text below.
Frequently Asked Questions
Does connection() run on the server or the client?
Should I use connection() with Cache Components?
Conclusion
connection() tells Next.js that rendering should wait for a real request. It is the explicit switch to dynamic rendering when no request-time API is present, and under Cache Components io() is usually the better tool.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.