How "use cache" works in multi-user apps
The "use cache" directive in Next.js caches the return value of a function or the rendered output of a component. By default, the cache is shared across all users, which raises a question: what happens when cached functions handle user-specific data? How do you prevent serving one user's data to another, and how do you structure a session-based application around a shared cache?
Where the cache lives
The "use cache" cache in Next.js is server-side. Depending on your hosting setup, it's stored in memory, on the file system, or in a managed cache layer (Vercel handles this automatically). It is not stored in the browser, and it is not scoped to individual users by default.
Cache entries persist across requests and are available to all server instances that share the same cache backend. On a single-server setup, that's the local memory or file system. On platforms like Vercel or with a custom "use cache: remote"handler (Redis, KV), the cache is shared across instances. For a full comparison of the three cache directives, see the Cache Components overview.
The default: shared cache
With "use cache", the function's arguments automatically become the cache key. Every caller who passes the same arguments gets the same cached result.
import { cacheTag, cacheLife } from 'next/cache'
async function getProduct(id: string) {
'use cache'
cacheTag(`product-${id}`)
cacheLife('days')
return db.products.findUnique({ where: { id } })
}
A thousand users requesting product #4816 all hit the same cache entry. The database query runs once, and every subsequent request serves the cached result until the entry is invalidated. This is the right approach for public content: product listings, blog posts, documentation, marketing pages.
User-specific data in cached functions
When you need to cache data that varies per user, there are a few things to be aware of.
First, the framework protects you from the most obvious mistake. Functions like cookies() and headers() provide data from the current request, which is fundamentally incompatible with a shared cache. A cached result is meant to be reused across requests, so it can't depend on request-specific values. Calling these functions inside a "use cache" scope throws an error:
async function getUserProfile() {
'use cache'
// This throws — cookies() is not available inside "use cache"
const session = (await cookies()).get('session')?.value
return db.users.findUnique({ where: { session } })
}
You can, however, pass a user ID as an argument. The ID becomes part of the cache key, so each user gets their own cache entry:
async function getUser(userId: string) {
'use cache'
cacheTag(`user-${userId}`)
cacheLife('hours')
return db.users.findUnique({ where: { id: userId } })
}
This works correctly, each user ID produces a separate entry. But be aware that the cached data sits in the server-side cache (memory or disk) and may contain sensitive information. Whether that's acceptable depends on your application and infrastructure. For public profile data it's likely fine. For data that includes email addresses or even payment information, consider whether server-side caching is appropriate for your setup.
Per-user caching with "use cache: private"
"use cache: private" is designed for content that varies per user and benefits from caching across requests from the same client. Unlike the default "use cache", the cache is scoped per-client (browser), not shared across all users on the server. It is not stored in the shared server cache alongside other users' data.
Because the scope is per-client, "use cache: private" can access cookies() and headers() inside the cached function:
import { cacheLife } from 'next/cache'
import { cookies } from 'next/headers'
async function getCartItems() {
'use cache: private'
cacheLife({ stale: 60 })
const session = (await cookies()).get('session')?.value || 'guest'
return db.cart.findMany({ where: { sessionId: session } })
}
This is useful when personalized content benefits from caching but shouldn't be shared between users. The cacheLife controls how long the entry stays valid for that specific client. See the Data-Level vs. UI-Level Caching article for more on when to use each directive.
Best practices for session-based apps
The key principle: keep user-specific data fetching in the components that actually need it, not at the page level.
A page that checks auth at the top and passes the session down to every child forces the entire tree to depend on the request. Instead, let cached components handle public content independently, and wrap user-specific components in <Suspense>:
import { Suspense } from 'react'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<div>
{/* Cached: same for everyone */}
<ProductDetails id={id} />
{/* Streamed: per-user */}
<Suspense fallback={<CartSkeleton />}>
<MiniCart />
</Suspense>
</div>
)
}
ProductDetails uses "use cache" internally and never touches the session. MiniCart reads cookies to get the cart for the current user and streams in at request time. The cached component and the dynamic component coexist on the same page without interfering with each other.
For cases where multiple components need the same user-specific data within one render, React's cache() function provides request-level deduplication without server-side persistence:
import { cache } from 'react'
import { cookies } from 'next/headers'
export const getCurrentUser = cache(async () => {
const session = (await cookies()).get('session')?.value
if (!session) return null
return db.users.findUnique({ where: { session } })
})
cache() from React deduplicates the call within a single render pass. If getCurrentUser() is called in the layout, the page, and a nested component, the database query runs once. The result is not persisted between requests, so there's no risk of serving stale or incorrect user data.
Cache entries at scale
Each unique combination of function + arguments produces one cache entry:
getProduct("42")called by 1,000 users = 1 cache entrygetProduct(id)called with 1,000 different IDs = 1,000 cache entries
The total number of cache entries is determined by the number of unique argument combinations, not the number of users. For public content with a bounded set of IDs (products, blog posts, pages), this scales well. For functions where the argument space is unbounded, set a reasonable cacheLife so entries expire and don't accumulate indefinitely.
For the full picture on Cache Components, including revalidation strategies and migration from the previous caching model, see the Cache Components series.