EdgeCases Logo
Nov 2025
Next.js
Deep
9 min read

Next.js 'use cache': Explicit Caching with Automatic Keys

Cache pages, components, and functions with opt-in directive and stale-while-revalidate profiles

nextjs
caching
performance
react
server-components
ppr
edge-case
next16

Next.js 16 introduces "use cache"—an explicit directive that caches pages, components, or functions with automatic cache key generation. Unlike previous implicit caching, all dynamic code runs at request time by default. You opt-in to caching where performance matters.

The Caching Inversion

Next.js 15 and earlier aggressively cached everything unless you explicitly opted out with export const dynamic = 'force-dynamic'. This caused confusion when data didn't update as expected. Next.js 16 inverts this:

// Without "use cache": Runs on every request
export default async function BlogPage() {
  const posts = await db.posts.findAll(); // Fresh query each time
  return <PostList posts={posts} />;
}

// With "use cache": Cached with automatic key generation
'use cache';
export default async function BlogPage() {
  const posts = await db.posts.findAll(); // Cached result
  return <PostList posts={posts} />;
}

Cache Scope: Three Levels

1. Page-Level Caching

// app/blog/page.tsx
'use cache';
export const cacheLife = 'max'; // Use predefined profile

export default async function BlogPage() {
  const posts = await db.posts.findAll();
  return <PostList posts={posts} />;
}

Entire page output is cached. Equivalent to static site generation but with runtime control.

2. Component-Level Caching

// components/UserProfile.tsx
'use cache';
export const cacheLife = 'default';

export async function UserProfile({ userId }: { userId: string }) {
  const user = await db.users.findById(userId); // Cached per userId
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

Cache key automatically includes userId prop. Each unique userId gets its own cache entry. This completes Partial Pre-Rendering (PPR)—cache expensive components while keeping others dynamic.

3. Function-Level Caching

'use cache';
export const cacheLife = {
  stale: 900,     // 15 minutes
  revalidate: 86400, // 1 day
  expire: 604800  // 7 days (hard expiration)
};

export async function getPopularPosts(category: string) {
  const posts = await db.posts.findPopular(category);
  return posts;
}

// Usage (in any component)
const posts = await getPopularPosts('nextjs'); // Cached per category

Cache Keys: Automatic Generation

Next.js generates cache keys from function arguments and component props. No manual key management:

'use cache';
async function fetchProduct(id: string, locale: string) {
  // Cache key: "fetchProduct:id=123:locale=en"
  return await db.products.findOne({ id, locale });
}

// Different arguments = different cache entries
await fetchProduct('123', 'en'); // Cache miss → DB query
await fetchProduct('123', 'fr'); // Cache miss → DB query
await fetchProduct('123', 'en'); // Cache hit → instant

cacheLife Profiles

Predefined profiles for common caching strategies:

export const cacheLife = 'default';
// stale: 900s (15min), revalidate: 86400s (1 day), expire: infinite

export const cacheLife = 'max';
// stale: 604800s (7 days), revalidate: 31536000s (1 year), expire: infinite

export const cacheLife = 'minutes';
// stale: 60s, revalidate: 600s (10min), expire: 3600s (1 hour)

Custom profiles in next.config.ts:

// next.config.ts
const nextConfig = {
  cacheLife: {
    blog: {
      stale: 3600,      // 1 hour stale
      revalidate: 86400, // Revalidate daily
      expire: 604800    // Hard expire after 7 days
    }
  }
};

// Usage
'use cache';
export const cacheLife = 'blog';

Stale-While-Revalidate Behavior

Cache lifecycle with cacheLife = 'default':

  1. 0–15 min: Serve cached version instantly (fresh)
  2. 15 min–1 day: Serve stale cached version + trigger background revalidation
  3. After 1 day: Cache expires, next request generates fresh version and caches it
'use cache';
export const cacheLife = 'default';

export async function NewsWidget() {
  const news = await fetch('https://api.news.com/latest');

  // t=0min: Cache miss → Fetch and cache
  // t=10min: Cache hit → Instant response (fresh)
  // t=20min: Cache hit → Instant stale response + revalidate in background
  // t=2days: Cache expired → Fresh fetch and cache

  return <NewsList items={news} />;
}

Edge Cases and Gotchas

Cache Keys Include All Props

Objects and arrays in props affect cache keys. Identical data with different object references = cache miss:

'use cache';
function ProductCard({ filters }: { filters: { category: string } }) {
  // Cache key includes filters object structure
}

// Cache miss (different object references)
<ProductCard filters={{ category: 'electronics' }} />
<ProductCard filters={{ category: 'electronics' }} />

// Solution: Stable references
const filters = { category: 'electronics' };
<ProductCard filters={filters} />
<ProductCard filters={filters} /> // Cache hit

No Caching for Mutations

"use cache" only works with read operations. Server Actions and mutations cannot be cached:

// ❌ Error: Server Actions cannot use "use cache"
'use cache';
export async function updateUser(formData: FormData) {
  await db.users.update(formData);
}

// ✓ Correct: Only cache reads
'use cache';
export async function getUser(id: string) {
  return await db.users.findById(id);
}

Request Context Breaks Caching

Accessing request-specific APIs (cookies(), headers()) inside cached functions disables caching:

'use cache'; // Has no effect due to cookies() call
export async function UserGreeting() {
  const cookieStore = await cookies();
  const name = cookieStore.get('username');

  return <h1>Hello {name}</h1>;
}

// Solution: Move request context out of cached boundary
export async function UserGreeting() {
  const cookieStore = await cookies();
  const name = cookieStore.get('username');

  return <CachedGreeting name={name} />;
}

'use cache';
async function CachedGreeting({ name }: { name: string }) {
  return <h1>Hello {name}</h1>;
}

Revalidation Strategies

On-Demand: revalidateTag()

import { revalidateTag } from 'next/cache';

'use cache';
export const cacheLife = 'max';
export const cacheTags = ['blog-posts'];

export async function BlogList() {
  const posts = await db.posts.findAll();
  return <PostList posts={posts} />;
}

// Trigger revalidation from Server Action
export async function publishPost(formData: FormData) {
  await db.posts.create(formData);
  await revalidateTag('blog-posts', 'max'); // Requires cacheLife profile
}

Read-Your-Writes: updateTag()

New API that expires cache and immediately fetches fresh data in the same request:

import { updateTag } from 'next/cache';

export async function saveUserSettings(formData: FormData) {
  await db.users.updateSettings(formData);

  // Expire cache + fetch fresh data immediately (no stale response)
  await updateTag('user-settings');

  // User sees updated data without page refresh
}

Migration from Next.js 15

// Next.js 15: Implicitly cached
export default async function Page() {
  const posts = await fetch('https://api.example.com/posts');
  return <PostList posts={posts} />;
}

// Next.js 16: Add "use cache" to maintain caching
'use cache';
export const cacheLife = 'default';
export default async function Page() {
  const posts = await fetch('https://api.example.com/posts');
  return <PostList posts={posts} />;
}

Finding Pages That Need Caching

  1. Run next build and check output for Dynamic vs Static routes
  2. Add "use cache" to routes that should be static
  3. Monitor response times in production—add caching where latency spikes

Configuration

// next.config.ts
const nextConfig = {
  // Enable Cache Components (required for component/function caching)
  cacheComponents: true,

  // Custom cache profiles
  cacheLife: {
    frequent: {
      stale: 60,        // 1 minute
      revalidate: 900,  // 15 minutes
      expire: 3600      // 1 hour
    },
    rare: {
      stale: 86400,     // 1 day
      revalidate: 604800, // 7 days
      expire: 2592000   // 30 days
    }
  }
};

export default nextConfig;

Cache Components shift caching from framework magic to developer intent. If data should be cached, say so explicitly. If it should be fresh, don't add the directive. Predictable, debuggable, and aligned with how caching works in every other framework.

Advertisement

Related Insights

Explore related edge cases and patterns

Next.js
Surface
Next.js 16: Dynamic by Default, Turbopack Stable, proxy.ts
8 min
Next.js
Deep
Turbopack: Next.js 16 Default Bundler (2-10× Faster)
8 min

Advertisement