Cache scopes
One probe — a random.org integer, different on every upstream call — through four different caches. Any two panels showing the same number share a cache entry; any two showing different numbers do not.
The private panel is the only one permitted to read cookies() and headers() inside its own cached scope. The other three throw next-request-in-use-cache if they try, and the restriction follows the call stack — a helper that reads a cookie fails the same way.
What to check
Shared against private. Open this page in two different browsers, or one normal and one private window. The shared and remote panels show the same number in both; the private panel differs per client.
Private is not persisted. Reload the private window. The private panel changes even inside its stale window, because nothing was written to a server cache — the client cache does not survive a page load.
Two handlers, verified. CACHE_DEBUG=true pnpm start and watch the handler logs while reloading. The shared and remote panels should log lookups; the private panel should log nothing.
The legacy path. Invalidate scope:legacy-fetch below. That tag only exists on a fetch(…, { next: { tags } }) call, so if that panel moves, the singular cacheHandler is alive and honouring tags.
Shared server caches
'use cache'
use cache- cacheLife
- blog
- cacheTag
- scope:shared
- upstream
- 298ms
ran at exec 411alaage …
probe 390,805
Stored via cacheHandlers.default. Shared across every visitor and every server instance.
'use cache: remote'
use cache: remote- cacheLife
- blog
- cacheTag
- scope:remote
- upstream
- 232ms
ran at exec 50zfl5age …
probe 311,817
Stored via cacheHandlers.remote. Durable and shared across all server instances.
Per-client and legacy
connection(). They need <Suspense>.'use cache: private'
suspense fallbackstreaming at request time — this is what ships in the static shell
fetch force-cache (legacy)
suspense fallbackstreaming at request time — this is what ships in the static shell
Invalidate per scope
Each scope has its own tag
revalidateTag('scope:shared', { expire: 0 })— Only the 'use cache' panel should move.revalidateTag('scope:remote', { expire: 0 })— Only the remote panel should move — proof the two handler slots hold separate entries.revalidateTag('scope:legacy-fetch', { expire: 0 })— Tagged via fetch's next.tags rather than cacheTag. If this works, the legacy cacheHandler is honouring tags.
The three directives, side by side
| use cache | use cache: remote | use cache: private | |
|---|---|---|---|
| Server-side storage | cacheHandlers.default | cacheHandlers.remote | none |
| Cache scope | all users | all users | one client |
| May read cookies()/headers() | no — pass as arguments | no — pass as arguments | yes |
| In the static shell | yes, if long-lived enough | yes, if long-lived enough | never |
| Invalidated by cacheTag | yes | yes | no |
| Survives a page reload | yes | yes | no |
| Survives a deploy | no | no | n/a |
Reach for remote when a scope resolves at request time rather than in the shell — each serverless instance has its own memory, so a shared store is what lifts the hit rate. For shell content, plain use cache is usually enough.