'use cache: private'

Cache per-user output that reads cookies, headers, or searchParams, in the browser only.

Signature
'use cache: private'
  • Added in Next.js v16.0.0

Return value

Nothing directly; the covered function returns its own value, cached only in the browser's memory for the current client.

Errors

Calling connection() inside the scope is prohibited, as is nesting a 'use cache: remote' scope inside it (and a private scope inside a remote one). Draft Mode enable()/disable() inside any cache directive scope also throws.

Changelog

  • PHP v16.0.0"use cache: private" is enabled with the Cache Components feature.

'use cache: private' is the one caching directive allowed to read cookies(), headers() and searchParams. The trade is that results are never stored on the server — they live in the browser's memory for that one client and do not survive a page reload.

Reach for it only when

Refactoring is impractical (the runtime access is buried deep in a function you cannot easily change), or compliance forbids putting the data on the server even briefly. For everything else, read the request value outside a plain 'use cache' scope and pass it in — cache on the dimension with few unique values, such as language, rather than on the user.

What trips people up

  • It is not a server cache. The function runs on every server render and is excluded from static shell generation; the win is on the client, across navigations within a session.
  • connection() is banned here as well as in 'use cache', because connection-specific information cannot be safely cached.
  • Custom cache handlers cannot be configured for it, and it cannot nest with 'use cache: remote' in either direction.
  • The stale time you set decides whether prefetching works at all: under 30 seconds and per-link prefetch is skipped; under 5 minutes and the content stays out of the route's App Shell.

Example

Example · typescript
import { Suspense } from 'react'
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'

export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params

  return (
    <div>
      <ProductDetails id={id} />
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <Recommendations productId={id} />
      </Suspense>
    </div>
  )
}

async function Recommendations({ productId }: { productId: string }) {
  const recommendations = await getRecommendations(productId)

  return (
    <div>
      {recommendations.map((rec) => (
        <ProductCard key={rec.id} product={rec} />
      ))}
    </div>
  )
}

async function getRecommendations(productId: string) {
  'use cache: private'
  cacheTag(`recommendations-${productId}`)
  cacheLife({ stale: 60 })

  // Request APIs are allowed inside a private cache
  const sessionId = (await cookies()).get('session-id')?.value || 'guest'

  return getPersonalizedRecommendations(productId, sessionId)
}

When to use it

  • Cache personalized recommendations that key off a session cookie, across client navigations.
  • Keep user-identifying data out of any server cache for a compliance requirement.
  • Cache a component that reads searchParams without refactoring it to take arguments.

Frequently asked questions

How is 'use cache: private' different from 'use cache'?

A private cache may read cookies(), headers() and searchParams, and its results are never stored on the server — they are cached only in the browser's memory, per client, and do not persist across page reloads. A regular 'use cache' entry is stored server-side and shared across all users, which is why it cannot touch request APIs.

Can I use a custom cache handler with 'use cache: private'?

No. It is not possible to configure custom cache handlers for 'use cache: private', because nothing is stored on the server in the first place. Custom handlers apply to 'use cache' and 'use cache: remote'.

Does 'use cache: private' content get prerendered?

No. Because it accesses runtime data, the function executes on every server render and is excluded from static shell generation. The stale time still matters on the client: at least 30 seconds for per-link prefetching to work, and at least 5 minutes to be included in the route's App Shell.

More examples

What each directive may read

The table is the entire reason this directive exists. connection() is refused by both, because it exposes connection-specific information that cannot be safely cached under any scope.

Example · typescript
// API             'use cache'   'use cache: private'
// cookies()       No            Yes
// headers()       No            Yes
// searchParams    No            Yes
// connection()    No            No

import { cookies } from 'next/headers'

async function shared() {
  'use cache'
  // Throws: next-request-in-use-cache
  const theme = (await cookies()).get('theme')?.value
  return theme
}

async function personal() {
  'use cache: private'
  // Allowed
  const theme = (await cookies()).get('theme')?.value
  return theme
}

The refactor to prefer

Lives at app/components/welcome-message.tsx. This produces a handful of shared server-side entries — one per language — instead of one per session. It is the recommended shape whenever the personalization can be reduced to a dimension with few values.

Example · typescript
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'

export async function WelcomeMessage() {
  // Extract the low-cardinality dimension...
  const language = (await cookies()).get('language')?.value || 'en'

  // ...and cache on that instead of on the user
  const content = await getCMSContent(language)

  return <div>{content.welcomeMessage}</div>
}

async function getCMSContent(language: string) {
  'use cache'
  cacheLife('hours')

  return cms.getHomeContent(language)
}

Three directives on one page

Lives at app/product/[id]/page.tsx. Each directive covers a different sharing scope on the same page: build-time and shared, runtime and shared, request-time and private. Remote and private may not nest inside one another, but they can sit side by side like this.

Example · typescript
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'

// Prerendered, shared by everyone
async function getProduct(id: string) {
  'use cache'
  cacheTag(`product-${id}`)
  return db.products.find({ where: { id } })
}

// Runtime, shared across server instances
async function getProductPrice(id: string) {
  'use cache: remote'
  cacheTag(`product-price-${id}`)
  cacheLife({ expire: 300 })
  return db.products.getPrice({ where: { id } })
}

// Per user, browser memory only
async function getRecommendations(productId: string) {
  'use cache: private'
  cacheLife({ expire: 60 })
  const sessionId = (await cookies()).get('session-id')?.value
  return db.recommendations.findMany({ where: { productId, sessionId } })
}

Discussion

  • Be the first to comment on this lesson.