Cache scopes
One probe, a random.org integer that differs on every upstream call, goes through five different caches. Two panels that show the same number share a cache entry.
The private panel is the only one permitted to read cookies() and headers() inside its own cached scope. The other four throw next-request-in-use-cache if they try, and the restriction follows the call stack, so 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, because it re-executes on every request.
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 and the client cache does not survive a page load.
Check both handlers. Run 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. Do the same with scope:unstable-cache for the unstable_cache panel.
Shared server caches
'use cache'
use cache- cacheLife
- blog
- cacheTag
- scope:shared
- upstream
- 246ms
ran at exec 37eov8age …
probe 117,154
Stored via cacheHandlers.default. Shared across every visitor and every server instance.
'use cache: remote'
use cache: remote- cacheLife
- blog
- cacheTag
- scope:remote
- upstream
- 209ms
ran at exec 6fz9wgage …
probe 137,540
Stored via cacheHandlers.remote. Durable and shared across all server instances.
Per-client and legacy
connection() first. All three 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.
unstable_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.revalidateTag('scope:legacy-fetch', { expire: 0 }): Tagged with fetch's next.tags instead of cacheTag. If the panel moves, the legacy cacheHandler honours tags.revalidateTag('scope:unstable-cache', { expire: 0 }): Tagged through unstable_cache's options. Only the unstable_cache panel should move.
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 |
Use remote when a scope resolves at request time instead of in the shell. Each serverless instance has its own memory, so a shared store raises the hit rate. For shell content, plain use cache is usually enough.