A cached scope cannot read the incoming request. cookies() and headers() are request-time APIs, and Next.js blocks them inside use cache because a cached result must be identical for every visitor.
Reading request data would make the entry per-request, which defeats the point. The point of caching is to compute once and reuse for many visitors, so anything that differs per visitor has no place in the cached body.
The failure surfaces as the next-request-in-use-cache error, which Next.js documents under this title:
Cannot access `cookies()` or `headers()` in `"use cache"`The restriction follows the call stack, so even a helper the cached function calls fails the same way. A cached entry is keyed by its inputs, and a request value changes on every call, so the entry could never be reused.
The broken version
The mistake is easy to make, because the directive and the cookie read sit in the same function. This function reads a cookie inside the cached scope and cannot prerender:
// app/lib/preferences.ts
import { cookies } from 'next/headers'
export async function getPreferences() {
'use cache'
const theme = (await cookies()).get('theme')?.value || 'light'
const res = await fetch(`https://api.example.com/preferences?theme=${theme}`)
return res.json()
}On a dynamically rendered route this may pass the build and then fail under next start, because the request data is not available while prerendering. The subtle part is that the build itself can look green while the production server is the one that errors.
The fix: pass values in
Read the cookie outside the cached scope and pass the extracted value as an argument.
// app/lib/preferences.ts
import { cookies } from 'next/headers'
export async function getPreferences(theme: string) {
'use cache'
const res = await fetch(`https://api.example.com/preferences?theme=${theme}`)
return res.json()
}
export async function readPreferences() {
const theme = (await cookies()).get('theme')?.value || 'light'
return getPreferences(theme)
}The theme value now joins the cache key, so each theme gets its own shared entry. The caller reads the request, the cached function only sees a plain string, and it can prerender again. The same approach works for any request value: extract it outside, then hand the cached function a plain argument.
What about searchParams
The same rule applies to searchParams. Read the value in the page, then pass it into the cached function rather than reading the promise inside the cached scope.
Route params follow the same boundary: await the promise in the page and hand the cached function the plain value. Draft mode is the one partial exception, because you may read its isEnabled flag inside a cached scope. Static values, arguments, and captured module-level data are all fine, only request-time data is blocked.
When you truly need request data inside
For the rare case where you cannot refactor, the use cache: private variant allows reading cookies, headers, and searchParams inside the scope, but it stores results only in the browser and never on the server. That is the tradeoff: you keep the request access but lose shared caching.
See use cache: private explained.
For where the directive can be placed, see the use cache directive.
Rune AI
Key Insights
- cookies(), headers(), and searchParams are blocked inside use cache.
- Reading them fails with the next-request-in-use-cache error.
- The fix is to read outside and pass values as arguments.
- Passed arguments become part of the cache key.
- use cache: private is the exception for browser-only results.
Frequently Asked Questions
Is searchParams also blocked inside use cache?
What is the fix when I genuinely need per-user data?
Conclusion
Cached scopes must be deterministic, so request APIs like cookies, headers, and searchParams are blocked inside use cache. Read them outside the scope and pass the extracted values as arguments, which also makes them part of the cache key.
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.