How to conditionally cache based on authentication?

This post might have stale content, as HEAD is 253 commits ahead.

How do I handle private/authenticated content with "use cache" in Next.js 16? I have pages that should be cached for public repos but NOT cached for private repos (require authentication). How do I conditionally skip caching or make it per-user?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Perfect! Now I have enough information to provide a comprehensive answer. Let me create a clear response.

##How to Handle Private/Authenticated Content with "use cache" in Next.js 16

Based on the Next.js 16 source code and documentation, here's how to handle the caching scenario you described:

###The Core Pattern: Use "use cache: private"

For content that requires authentication and should NOT be cached server-side, use 'use cache: private':

// app/repo/page.tsx
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import { Suspense } from 'react'

export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <RepoContent />
    </Suspense>
  )
}

async function RepoContent() {
  'use cache: private'
  cacheLife({ stale: 60 }) // 60 seconds minimum for runtime prefetch
  
  // Can access cookies for authentication
  const token = (await cookies()).get('auth-token')?.value
  
  // Fetch based on authentication
  const repo = await fetch(`/api/repo`, {
    headers: { Authorization: `Bearer ${token}` }
  }).then(r => r.json())
  
  return <RepoDisplay repo={repo} />
}

Key characteristics of 'use cache: private':

AspectBehavior
Server-side cache❌ None - never stored on server
Browser cache✅ Cached only in browser memory during session
Runtime API access✅ Can access cookies(), headers(), searchParams
Persists across reloads❌ No (browser-only cache)
Per-user caching✅ Yes - automatically per-user
Static shell prerendering❌ Excluded (always runs at runtime)

###Pattern: Public vs Private Content

For repos with mixed public/private access:

// app/repos/[owner]/[name]/page.tsx
import { cookies } from 'next/headers'
import { cacheLife, cacheTag } from 'next/cache'
import { Suspense } from 'react'

export default async function RepoPage({
  params,
}: {
  params: Promise<{ owner: string; name: string }>
}) {
  const { owner, name } = await params

  return (
    <>
      {/* Public content - cached on server for all users */}
      <PublicRepoInfo owner={owner} name={name} />

      {/* Private content - only cached in browser per-user */}
      <Suspense fallback={<p>Loading private data...</p>}>
        <PrivateRepoData owner={owner} name={name} />
      </Suspense>
    </>
  )
}

// ✅ PUBLIC: Shared across all users
async function PublicRepoInfo({
  owner,
  name,
}: {
  owner: string
  name: string
}) {
  'use cache' // Regular cache - server-side
  cacheLife('hours')
  cacheTag(`repo-${owner}-${name}`)

  const repo = await db.repos.findPublic(owner, name)
  return <div>{repo.description}</div>
}

// 🔒 PRIVATE: Per-user, browser-only
async function PrivateRepoData({
  owner,
  name,
}: {
  owner: string
  name: string
}) {
  'use cache: private' // Browser-only, per-user cache
  cacheLife({ stale: 120 }) // Must be >= 30 seconds
  
  const token = (await cookies()).get('github-token')?.value
  const userId = (await cookies()).get('user-id')?.value

  if (!token) {
    return <p>Please log in</p>
  }

  // Will execute for each unique user at runtime
  const access = await checkAccess(owner, name, userId, token)
  
  return access.isPrivate ? (
    <PrivateContent repoData={access.data} />
  ) : null
}

###Alternative: Conditional Wrapper Pattern

If you want to conditionally choose between 'use cache' and 'use cache: private' based on authentication status:

// app/repo/conditional-page.tsx
import { cookies } from 'next/headers'
import { cacheLife } from 'next/cache'
import { Suspense } from 'react'

export default async function ConditionalPage() {
  // Check authentication at the page level
  const token = (await cookies()).get('auth-token')?.value
  const isAuthenticated = !!token

  return (
    <div>
      <h1>Repository Content</h1>
      <Suspense fallback={<p>Loading...</p>}>
        {isAuthenticated ? (
          <PrivateCachedContent token={token} />
        ) : (
          <PublicCachedContent />
        )}
      </Suspense>
    </div>
  )
}

// For public visitors
async function PublicCachedContent() {
  'use cache'
  cacheLife('hours')
  
  const data = await fetch('/api/public-repo')
  return <div>{/* render public content */}</div>
}

// For authenticated users
async function PrivateCachedContent({ token }: { token: string }) {
  'use cache: private'
  cacheLife({ stale: 60 })
  
  // token is passed as argument, part of cache key
  const data = await fetch('/api/private-repo', {
    headers: { Authorization: `Bearer ${token}` }
  })
  return <div>{/* render private content */}</div>
}

###Important Notes

  1. 'use cache: private' cannot be conditional - The directive itself must be static. You must restructure your component tree to have separate components for each caching strategy.

  2. Minimum stale time - Private caches require at least 30 seconds for cacheLife.stale (this is a limitation for runtime prefetching).

  3. Pass authentication data as arguments - For 'use cache' (not private), you cannot read cookies() directly. Instead, extract and pass values:

// ❌ WRONG - for regular 'use cache'
async function CachedFunction() {
  'use cache'
  const token = (await cookies()).get('token') // ERROR!
}

// ✅ CORRECT
async function CachedFunction({ token }: { token: string }) {
  'use cache'
  // token is now part of the cache key
}

// Pass the token from the parent
const token = (await cookies()).get('token')?.value
return <CachedFunction token={token} />
  1. 'use cache: private' cannot be customized - Unlike 'use cache' and 'use cache: remote', you cannot configure custom cache handlers for private caches.

###Files to Reference

  • Source: packages/next/src/server/use-cache/use-cache-wrapper.ts - Shows that private caches are never stored in cache handlers (line 1664)
  • Docs: docs/01-app/03-api-reference/01-directives/use-cache-private.mdx - Official documentation
  • Test Example: test/e2e/app-dir/use-cache-private/ - Real-world examples of private cache usage

END OF POST

How to conditionally cache based on authentication? — vercel/next.js