Keep cookies() out of cache

Read cookies() in the uncached parent and pass the value in.

Next.js docsAPI reference · unstable_cache
unstable_cache (legacy): replaced by the use cache directive in Next.js 16.
ReferenceRead docs ↗

From next-cache-components, “Runtime Data Constraint”.

Build …

Pass the session in

Pick a session, request twice, then switch the directive.

Cookie session
cookies() insideBad
async function CachedProfile() {
  'use cache'
  const s = (await cookies()).get('session')
  return <div>{(await fetchUserData(s)).name}</div>
}
Press Request.
Passed in from parentGood
build… fn… args… …
Press Request.
Cache entries0
    <CachedProfile sessionId={session} />
    import { cookies } from 'next/headers'
    
    export default async function ProfilePage() {
      const session = (await cookies()).get('session')?.value
      return <CachedProfile sessionId={session} />
    }
    
    async function CachedProfile({ sessionId }: { sessionId?: string }) {
      'use cache'
      const user = await fetchUserData(sessionId)
      return <div>{user.name}</div>
    }

    Closures are part of the key

    Render as Ana, switch to Ben, render again.

    Component prop userId
    Argument filter
    unstable_cacheBad
    const getData = unstable_cache(
      async (filter) => db.users.find(userId, filter),
      ['user-data'],
      { tags: ['users'], revalidate: 60 }
    )
    keyParts['user-data'] args… userId… …
    Press Render.
    Cache entries0
      key = keyParts + args
      'use cache'Good
      const getData = async (filter) => {
        'use cache'
        return db.users.find(userId, filter)
      }
      build… fn… closure… args… …
      Press Render.
      Cache entries0
        key = userId + filter
        import { cacheLife, cacheTag } from 'next/cache'
        
        async function Component({ userId }: { userId: string }) {
          const getData = async (filter: string) => {
            'use cache'
            cacheTag('users')
            cacheLife({ revalidate: 60 })
            return db.users.find(userId, filter)
          }
          return <List items={await getData('active')} />
        }

        Random values run once

        Request a few times, then Deploy in the toolbar.

        Inside 'use cache'Bad
        async function RequestId() {
          'use cache'
          return { id: crypto.randomUUID(), at: Date.now() }
        }
        Responses0 requests
          build …
          After connection()Good
          async function RequestId() {
            await connection()
            return { id: crypto.randomUUID(), at: Date.now() }
          }
          Responses0 requests
            <Suspense>
            import { connection } from 'next/server'
            
            async function RequestId() {
              await connection()
              const id = crypto.randomUUID()
              return <span>{id}</span>
            }

            Why it matters, pros and cons

            Why it matters

            The key sees only arguments and closures, never a cookie read inside.

            Pros

            • Every input shows up in the key
            • No keyParts to forget
            • Cached functions stay pure and testable

            Cons

            • Each distinct argument adds a cache entry
            • Arguments must be serializable
            • Data nobody requests twice gains nothing from caching