MENU
Caching Configurations
1. cacheHandler & cacheMaxMemorySize
The cacheHandler option allows you to configure a custom cache implementation for Next.js, enabling you to persist cached pages and data to durable storage or share the cache across multiple containers or instances of your Next.js application.
By default, Next.js uses an in-memory cache that doesn't persist between deployments or share across instances. A custom cache handler is particularly useful for:
- Multi-instance deployments where you want shared cache
- Serverless environments where memory doesn't persist
- Integration with external cache systems like Redis, Memcached, or database-backed caches
To configure a custom cache handler:
module.exports = {
cacheHandler: require.resolve('./cache-handler.js'),
cacheMaxMemorySize: 0, // disable default in-memory caching
}
Your custom cache handler must implement the following methods:
- get(key) - Returns cached value or null
- set(key, data, ctx) - Stores data with optional cache tags
- revalidateTag(tag) - Invalidates cache entries by tag
- resetRequestCache() - Clears temporary request cache
Example Redis-based cache handler:
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class CustomCacheHandler {
async get(key) {
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(key, data, ctx) {
await redis.setex(key, 3600, JSON.stringify(data));
if (ctx?.tags) {
// Store reverse mapping for tag-based invalidation
for (const tag of ctx.tags) {
await redis.sadd(`tag:${tag}`, key);
}
}
}
async revalidateTag(tag) {
const keys = await redis.smembers(`tag:${tag}`);
if (keys.length > 0) {
await redis.del(...keys);
await redis.del(`tag:${tag}`);
}
}
resetRequestCache() {
// Clear any request-scoped cache
}
}
module.exports = CustomCacheHandler;
2. cacheLife
The cacheLife function is used in conjunction with the use cache directive to define how long cached data should remain valid. This is part of Next.js's experimental caching system that works with dynamicIO and useCache flags.
When you use the use cache directive, you can specify cache lifetime policies:
import { cacheLife } from 'next/cache';
export default async function ProductPage({ params }) {
'use cache';
cacheLife('max');
const product = await fetchProduct(params.id);
return <div>{product.name}</div>;
}
Available cache life policies include:
- 'max' - Cache indefinitely until manually invalidated
- 'default' - Use default cache duration
- Custom duration - Specify exact cache time
For dynamic content with specific cache requirements:
async function getWeatherData() {
'use cache';
cacheLife('1 hour');
return fetch('/api/weather').then(r => r.json());
}
This is particularly useful for:
- API responses that change infrequently
- Expensive computations that can be cached
- Database queries with predictable data patterns
3. dynamicIO
The dynamicIO flag is an experimental feature that changes how Next.js handles data fetching in the App Router. When enabled, data fetching operations are excluded from pre-renders unless explicitly cached with use cache.
This is useful when your application requires fresh data at runtime rather than serving pre-rendered content:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
},
}
export default nextConfig
With dynamicIO enabled, consider this component:
// Without 'use cache' - always fetches fresh data at runtime
export default async function UserDashboard() {
const user = await getCurrentUser(); // Always fresh
const notifications = await getNotifications(); // Always fresh
return (
<div>
<h1>Welcome, {user.name}</h1>
<NotificationList notifications={notifications} />
</div>
);
}
// With 'use cache' - can be pre-rendered
async function getStaticContent() {
'use cache';
return await fetchStaticContent();
}
This approach is beneficial for:
- Real-time dashboards
- User-specific content
- Applications with frequently changing data
- When you want explicit control over what gets cached vs. fetched fresh
Note that while dynamicIO ensures fresh data, it may introduce additional latency compared to pre-rendered content.
4. expireTime
The expireTime option configures a custom stale-while-revalidate expire time for CDNs in the Cache-Control header for ISR (Incremental Static Regeneration) enabled pages.
This setting helps CDNs understand how long they can serve stale content while revalidating in the background:
module.exports = {
// one hour in seconds
expireTime: 3600,
}
The expire time works with your page's revalidate setting. For example, if you have:
// pages/blog/[slug].js
export async function getStaticProps({ params }) {
const post = await fetchBlogPost(params.slug);
return {
props: { post },
revalidate: 900, // 15 minutes
};
}
With expireTime: 3600 (1 hour), Next.js generates:
Cache-Control: s-maxage=900, stale-while-revalidate=2700
This means:
- CDN serves fresh content for 15 minutes (s-maxage=900)
- CDN can serve stale content for additional 45 minutes while revalidating (stale-while-revalidate=2700)
- Total stale time = expireTime - revalidate = 3600 - 900 = 2700 seconds
This is particularly useful for:
- High-traffic sites where brief stale content is acceptable
- Reducing origin server load during revalidation
- Ensuring consistent performance during traffic spikes
5. generateEtags
The generateEtags option controls whether Next.js automatically generates ETags for HTML pages. ETags are HTTP response headers that help with browser caching by allowing the browser to check if content has changed.
By default, Next.js generates ETags for every page. You might want to disable this depending on your caching strategy:
module.exports = {
generateEtags: false,
}
Reasons to disable ETag generation:
- When using a CDN that handles ETags differently
- When you have custom caching logic that conflicts with ETags
- When you want to reduce response header size
- When deploying behind load balancers that might modify ETags
Example of ETag behavior:
// With generateEtags: true (default)
// Response headers include:
// ETag: "1234567890abcdef"
// Browser on subsequent request sends:
// If-None-Match: "1234567890abcdef"
// If content unchanged, server responds with 304 Not Modified
For applications with:
- Frequently changing dynamic content
- Complex caching layers
- Custom cache validation logic
You might prefer to handle cache validation manually rather than relying on automatic ETag generation.
6. onDemandEntries
The onDemandEntries option controls how Next.js manages built pages in memory during development. This affects development server performance and memory usage.
Configure the development page buffer settings:
module.exports = {
onDemandEntries: {
// period (in ms) where the server will keep pages in the buffer
maxInactiveAge: 25 * 1000, // 25 seconds
// number of pages that should be kept simultaneously without being disposed
pagesBufferLength: 2,
},
}
These settings control:
- maxInactiveAge - How long to keep a page in memory after last access
- pagesBufferLength - Maximum number of pages to keep in memory simultaneously
For different development scenarios:
// For large applications with many pages (reduce memory usage)
module.exports = {
onDemandEntries: {
maxInactiveAge: 10 * 1000, // 10 seconds
pagesBufferLength: 1, // Keep only 1 page
},
}
// For small teams frequently switching between pages (increase performance)
module.exports = {
onDemandEntries: {
maxInactiveAge: 60 * 1000, // 1 minute
pagesBufferLength: 5, // Keep 5 pages
},
}
This is particularly useful when:
- Working on large applications with memory constraints
- Developing on machines with limited RAM
- Team workflows involve frequent page switching
- You want to optimize development server startup time
7. serverComponentsHmrCache
The serverComponentsHmrCache is an experimental feature that caches fetch responses in Server Components across Hot Module Replacement (HMR) refreshes during local development.
This reduces API calls and speeds up development by caching responses between code changes:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
serverComponentsHmrCache: false, // defaults to true
},
}
export default nextConfig
When enabled (default), this affects Server Components like:
// This fetch will be cached across HMR refreshes
export default async function BlogPost({ slug }) {
const post = await fetch(`/api/posts/${slug}`, {
cache: 'no-store' // Even no-store requests are cached in HMR
}).then(r => r.json());
return <article>{post.content}</article>;
}
Benefits:
- Faster development iteration
- Reduced API calls during development
- Lower costs for billed external APIs
- Improved developer experience
Important considerations:
- Cache is cleared on navigation or full page reloads
- May show stale data between HMR refreshes
- Only affects development environment
- Applies to all fetch requests by default
For better observability, combine with logging:
const nextConfig: NextConfig = {
experimental: {
serverComponentsHmrCache: true,
},
logging: {
fetches: {
fullUrl: true,
},
},
}
8. staleTimes
The staleTimes experimental feature enables caching of page segments in the client-side router cache, controlling how long different types of content remain cached on the client.
Configure stale times for different content types:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30, // 30 seconds for dynamic content
static: 180, // 3 minutes for static content
},
},
}
module.exports = nextConfig
The two properties control different scenarios:
- dynamic - Used for pages that aren't statically generated or fully prefetched
- static - Used for statically generated pages or when prefetch={true} is used
Example usage with Next.js routing:
import Link from 'next/link';
export default function Navigation() {
return (
<nav>
{/* This link benefits from static stale time (180s) */}
<Link href="/about" prefetch={true}>
About
</Link>
{/* This link uses dynamic stale time (30s) */}
<Link href="/dashboard">
Dashboard
</Link>
</nav>
);
}
Benefits for different content types:
// Static marketing pages - longer cache time
staleTimes: {
static: 300, // 5 minutes
}
// Dynamic user dashboards - shorter cache time
staleTimes: {
dynamic: 15, // 15 seconds
}
// Balanced approach for mixed content
staleTimes: {
dynamic: 30, // 30 seconds
static: 180, // 3 minutes
}
Important notes:
- Doesn't affect back/forward caching behavior
- Loading boundaries are reusable for the static period
- Shared layouts won't be refetched automatically
- Helps maintain browser scroll position
9. useCache
The useCache flag is an experimental feature that enables the use cache directive to be used independently of dynamicIO. This gives you granular control over caching at the component and function level.
Enable the useCache flag:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
experimental: {
useCache: true,
},
}
export default nextConfig
Once enabled, you can use caching directives in your components:
import { cacheTag, cacheLife } from 'next/cache';
// Cache an entire page component
export default async function ProductCatalog() {
'use cache';
cacheLife('1 hour');
cacheTag('products', 'catalog');
const products = await fetchProducts();
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Cache a specific function
async function getExpensiveData(id) {
'use cache';
cacheLife('30 minutes');
cacheTag('expensive-computation');
return await performExpensiveComputation(id);
}
Available cache functions:
- use cache directive - Marks function/component for caching
- cacheLife(duration) - Sets cache expiration time
- cacheTag(...tags) - Adds tags for selective invalidation
Practical caching strategies:
// Short-lived user-specific data
async function getUserPreferences(userId) {
'use cache';
cacheLife('5 minutes');
cacheTag(`user-${userId}`, 'preferences');
return await fetchUserPreferences(userId);
}
// Long-lived static content
async function getSiteConfiguration() {
'use cache';
cacheLife('24 hours');
cacheTag('site-config');
return await fetchSiteConfig();
}
// Cache with manual invalidation
async function getBlogPost(slug) {
'use cache';
cacheLife('max'); // Cache until manually invalidated
cacheTag('blog-posts', `post-${slug}`);
return await fetchBlogPost(slug);
}
This approach enables:
- Fine-grained caching control
- Performance optimization for expensive operations
- Selective cache invalidation using tags
- Flexible cache lifetime policies