Caching is one of the most powerful performance optimizations in Next.js. By default, Next.js aggressively caches data to make your applications incredibly fast. Understanding when data is cached, how long it stays cached, and when to revalidate is essential for building applications that are both fast and fresh. Let's master Next.js caching strategies and learn to balance performance with data freshness!
Next.js Caching Overview
Next.js provides multiple layers of caching for optimal performance:
Caching Layers
- Request Memoization: Deduplicates identical requests in one render
- Data Cache: Persistent cache for fetch requests
- Full Route Cache: Cached HTML and RSC payload
- Router Cache: Client-side cache for visited routes
This Tutorial's Focus
We'll focus on the Data Cache (how fetch requests are cached) and revalidation strategies. This is what you control directly in your code.
Default Caching Behavior
By default, Next.js caches fetch() requests aggressively:
// Default behavior: cache forever
async function getData() {
const res = await fetch('https://api.example.com/data');
// ✅ Cached indefinitely
// ✅ Same as: fetch(url, { cache: 'force-cache' })
return res.json();
}
// This data is fetched ONCE at build time (or first request)
// Then served from cache forever
async function Page() {
const data = await getData();
return <div>{data.title}</div>;
}
// ✅ Extremely fast - served from cache
// ❌ Data never updates unless you rebuild or revalidate⚠️ Important: Default = Aggressive Caching
Next.js defaults to force-cache which caches indefinitely. This is great for static data but problematic for dynamic content. You must explicitly opt into revalidation or no-cache for data that changes.
Cache Options
Option 1: force-cache (Default)
Cache indefinitely until manually revalidated:
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'force-cache', // Explicit, but this is the default
});
return res.json();
}
// When to use:
// ✅ Static data that never changes
// ✅ Configuration data
// ✅ Country lists, categories, taxonomies
// ✅ Build-time data
// Examples:
async function getCountries() {
return fetch('https://api.example.com/countries', {
cache: 'force-cache',
}).then(r => r.json());
}
async function getCategories() {
return fetch('https://api.example.com/categories', {
cache: 'force-cache',
}).then(r => r.json());
}Option 2: no-store (Never Cache)
Always fetch fresh data, never cache:
async function getData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store', // Never cache, always fresh
});
return res.json();
}
// When to use:
// ✅ User-specific data (dashboards, profiles)
// ✅ Real-time data (stock prices, live scores)
// ✅ Personalized content
// ✅ Data that must always be current
// Examples:
async function getUserBalance(userId: string) {
return fetch(`https://api.example.com/users/${userId}/balance`, {
cache: 'no-store', // Always fresh
}).then(r => r.json());
}
async function getStockPrice(symbol: string) {
return fetch(`https://api.example.com/stocks/${symbol}`, {
cache: 'no-store', // Real-time price
}).then(r => r.json());
}
async function getNotifications(userId: string) {
return fetch(`https://api.example.com/users/${userId}/notifications`, {
cache: 'no-store', // User-specific, must be current
}).then(r => r.json());
}Option 3: Time-Based Revalidation
Cache for a specific time, then revalidate:
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Revalidate after 1 hour (in seconds)
});
return res.json();
}
// When to use:
// ✅ Data that changes occasionally
// ✅ Blog posts, news articles
// ✅ Product listings
// ✅ Semi-static content
// Examples:
async function getBlogPosts() {
return fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // 1 hour
}).then(r => r.json());
}
async function getProducts() {
return fetch('https://api.example.com/products', {
next: { revalidate: 300 }, // 5 minutes
}).then(r => r.json());
}
async function getNews() {
return fetch('https://api.example.com/news', {
next: { revalidate: 60 }, // 1 minute
}).then(r => r.json());
}
// Common revalidation times:
// 10 seconds: { revalidate: 10 } - Frequently changing
// 1 minute: { revalidate: 60 } - News, feeds
// 5 minutes: { revalidate: 300 } - Product listings
// 1 hour: { revalidate: 3600 } - Blog posts
// 24 hours: { revalidate: 86400 } - Nearly static contentOption 4: Disable Caching (Alternative Syntax)
async function getData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 0 }, // Same as cache: 'no-store'
});
return res.json();
}
// Both achieve the same result:
// Option 1: cache: 'no-store'
// Option 2: next: { revalidate: 0 }
// Use whichever is clearer in your contextHow Revalidation Works
Stale-While-Revalidate Strategy
Next.js uses a stale-while-revalidate approach for time-based revalidation:
Timeline Example (revalidate: 60)
• Data fetched from API
• Cached for 60 seconds
• User gets fresh data
• Within 60s window
• ✅ Served from cache instantly
• No API call made
• Past 60s window (stale)
• ✅ Stale cache returned immediately (fast!)
• 🔄 Background revalidation triggered
• New data fetched and cached
• ✅ Fresh data from cache
• Background revalidation completed
• Users now get updated data
Stale-While-Revalidate Benefits
- Instant response: Users get cached data immediately
- Always up-to-date: Fresh data fetched in background
- No waiting: Users never wait for API calls
- Best of both worlds: Speed of caching + freshness of real-time
async function BlogPage() {
// First request: Fetches and caches for 60 seconds
// Within 60s: Returns from cache
// After 60s: Returns stale cache + revalidates in background
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 60 },
}).then(r => r.json());
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
// ✅ Fast: Always returns cached data instantly
// ✅ Fresh: Updates in background when stale
// ✅ No loading states neededPractical Examples
Example 1: Blog with Different Cache Strategies
// Blog post list - revalidate every hour
async function getBlogPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // 1 hour
});
return res.json();
}
// Categories - cache forever (rarely change)
async function getCategories() {
const res = await fetch('https://api.example.com/categories', {
cache: 'force-cache', // Forever
});
return res.json();
}
// Trending posts - revalidate frequently
async function getTrendingPosts() {
const res = await fetch('https://api.example.com/posts/trending', {
next: { revalidate: 300 }, // 5 minutes
});
return res.json();
}
export default async function BlogPage() {
const [posts, categories, trending] = await Promise.all([
getBlogPosts(),
getCategories(),
getTrendingPosts(),
]);
return (
<div className="container mx-auto px-4 py-8">
<aside className="w-64">
<h2>Categories</h2>
{/* Cached forever */}
{categories.map(cat => (
<a key={cat.id} href={`/blog/${cat.slug}`}>
{cat.name}
</a>
))}
</aside>
<main className="flex-1">
<section className="mb-8">
<h2>Trending Now</h2>
{/* Revalidates every 5 minutes */}
{trending.map(post => (
<PostCard key={post.id} post={post} />
))}
</section>
<section>
<h2>All Posts</h2>
{/* Revalidates every hour */}
{posts.map(post => (
<PostCard key={post.id} post={post} />
))}
</section>
</main>
</div>
);
}
// ✅ Categories: Static, never revalidate
// ✅ Trending: Fresh (5 min), shows current trends
// ✅ Posts: Recent enough (1 hour), not too staleExample 2: E-commerce Product Page
// Product details - revalidate every 5 minutes
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 300 }, // 5 minutes
});
return res.json();
}
// Reviews - revalidate every hour
async function getReviews(productId: string) {
const res = await fetch(`https://api.example.com/products/${productId}/reviews`, {
next: { revalidate: 3600 }, // 1 hour
});
return res.json();
}
// Inventory - no cache (must be real-time)
async function getInventory(productId: string) {
const res = await fetch(`https://api.example.com/products/${productId}/inventory`, {
cache: 'no-store', // Always fresh
});
return res.json();
}
// Related products - cache for 1 hour
async function getRelatedProducts(productId: string) {
const res = await fetch(`https://api.example.com/products/${productId}/related`, {
next: { revalidate: 3600 }, // 1 hour
});
return res.json();
}
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const [product, reviews, inventory, related] = await Promise.all([
getProduct(params.id),
getReviews(params.id),
getInventory(params.id),
getRelatedProducts(params.id),
]);
return (
<div className="container mx-auto px-4 py-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Product Info - 5 min cache */}
<div>
<h1 className="text-4xl font-bold mb-4">{product.title}</h1>
<p className="text-3xl text-green-600 mb-4">${product.price}</p>
<p className="text-gray-700 mb-6">{product.description}</p>
{/* Inventory - real-time */}
<div className="mb-6">
{inventory.inStock ? (
<span className="text-green-600 font-semibold">
✓ In Stock ({inventory.quantity} available)
</span>
) : (
<span className="text-red-600 font-semibold">Out of Stock</span>
)}
</div>
<button className="w-full bg-blue-600 text-white py-3 rounded-lg">
Add to Cart
</button>
</div>
<div>
<img
src={product.image}
alt={product.title}
className="w-full rounded-lg"
/>
</div>
</div>
{/* Reviews - 1 hour cache */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">Reviews</h2>
<div className="space-y-4">
{reviews.map(review => (
<ReviewCard key={review.id} review={review} />
))}
</div>
</div>
{/* Related - 1 hour cache */}
<div className="mt-12">
<h2 className="text-2xl font-bold mb-6">You May Also Like</h2>
<div className="grid grid-cols-4 gap-6">
{related.map(item => (
<ProductCard key={item.id} product={item} />
))}
</div>
</div>
</div>
);
}
// ✅ Product: Fresh enough (5 min)
// ✅ Inventory: Real-time (critical for purchases)
// ✅ Reviews: Can be slightly stale (1 hour)
// ✅ Related: Can be stale (1 hour)Example 3: User Dashboard (No Cache)
// User data - no cache (user-specific)
async function getUserData(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}`, {
cache: 'no-store', // Always fresh
});
return res.json();
}
// User stats - no cache (personalized)
async function getUserStats(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/stats`, {
cache: 'no-store', // Always fresh
});
return res.json();
}
// Recent activity - no cache (must be current)
async function getRecentActivity(userId: string) {
const res = await fetch(`https://api.example.com/users/${userId}/activity`, {
cache: 'no-store', // Always fresh
});
return res.json();
}
export default async function DashboardPage({
params,
}: {
params: { userId: string };
}) {
const [user, stats, activity] = await Promise.all([
getUserData(params.userId),
getUserStats(params.userId),
getRecentActivity(params.userId),
]);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Welcome, {user.name}</h1>
{/* Stats */}
<div className="grid grid-cols-4 gap-6 mb-8">
<StatCard title="Balance" value={`$${stats.balance}`} />
<StatCard title="Orders" value={stats.orders} />
<StatCard title="Points" value={stats.points} />
<StatCard title="Level" value={stats.level} />
</div>
{/* Recent Activity */}
<div>
<h2 className="text-2xl font-bold mb-4">Recent Activity</h2>
<div className="space-y-3">
{activity.map(item => (
<ActivityItem key={item.id} item={item} />
))}
</div>
</div>
</div>
);
}
// ✅ All data user-specific - no cache
// ✅ Always shows current information
// ✅ Critical for personalized dashboardsCache Strategy Decision Guide
✅ Use force-cache (Cache Forever)
When: Data never or rarely changes
- Country lists, state/province lists
- Categories, taxonomies
- Configuration data
- Static content pages
- Reference data
{ cache: 'force-cache' } // or omit (default)✅ Use Time-Based Revalidation
When: Data changes occasionally
- Blog posts, articles (1-24 hours)
- Product listings (5-60 minutes)
- News feeds (1-5 minutes)
- Social media feeds (1-5 minutes)
- Analytics dashboards (5-60 minutes)
{ next: { revalidate: 300 } } // 5 minutes✅ Use no-store (No Cache)
When: Data must always be fresh
- User-specific data (dashboards, profiles)
- Real-time data (stock prices, live scores)
- Shopping cart, user balance
- Authentication data
- Personalized recommendations
{ cache: 'no-store' } // or { next: { revalidate: 0 } }⚠️ Common Mistake: Over-Caching
The default force-cache can cause issues if you forget to set revalidation for dynamic data. Always consider: "How fresh does this data need to be?" and set cache options accordingly.
Caching Strategy Examples
Different caching approaches for different data types
Select a file or folder to see details
Route Segment Config
You can also set caching at the page/route level:
// Set revalidation for entire route
export const revalidate = 3600; // 1 hour
// Or disable caching for entire route
export const dynamic = 'force-dynamic'; // Same as no-store for all fetches
// Or force static (cache forever)
export const dynamic = 'force-static'; // Same as force-cache for all fetches
export default async function BlogPage() {
// All fetch calls in this route inherit the settings above
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json());
return <div>{/* render */}</div>;
}
// Route segment config applies to all fetches in the route
// Individual fetch calls can still overrideRoute Config Options
// Revalidate time in seconds
export const revalidate = 60; // Revalidate every 60 seconds
// Dynamic rendering
export const dynamic = 'auto'; // Default: auto-detect
export const dynamic = 'force-dynamic'; // Always dynamic (no cache)
export const dynamic = 'force-static'; // Always static (cache forever)
export const dynamic = 'error'; // Error if dynamic
// Fetch cache
export const fetchCache = 'auto'; // Default
export const fetchCache = 'default-cache'; // Cache by default
export const fetchCache = 'only-cache'; // Only cached responses
export const fetchCache = 'force-cache'; // Force cache all
export const fetchCache = 'default-no-store'; // No cache by default
export const fetchCache = 'only-no-store'; // Only fresh responses
export const fetchCache = 'force-no-store'; // Force no cache allCaching Best Practices
1. Match Cache Strategy to Data Type
// ✅ GOOD: Different strategies for different data
async function Page() {
const [static, semiStatic, dynamic] = await Promise.all([
// Static: Cache forever
fetch('https://api.example.com/countries', {
cache: 'force-cache',
}),
// Semi-static: Revalidate periodically
fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
}),
// Dynamic: No cache
fetch('https://api.example.com/user', {
cache: 'no-store',
}),
]);
}
// ❌ BAD: Same strategy for all data
async function Page() {
const [countries, posts, user] = await Promise.all([
// All no-cache - wastes resources
fetch('https://api.example.com/countries', { cache: 'no-store' }),
fetch('https://api.example.com/posts', { cache: 'no-store' }),
fetch('https://api.example.com/user', { cache: 'no-store' }),
]);
}2. Use Appropriate Revalidation Times
// ✅ GOOD: Revalidation times match data change frequency
const blogPosts = await fetch('url', {
next: { revalidate: 3600 }, // 1 hour - posts don't change often
});
const stockPrices = await fetch('url', {
cache: 'no-store', // Real-time - always fresh
});
const categories = await fetch('url', {
cache: 'force-cache', // Static - never change
});
// ❌ BAD: Revalidation too aggressive or too conservative
const blogPosts = await fetch('url', {
next: { revalidate: 1 }, // 1 second - way too aggressive!
});
const stockPrices = await fetch('url', {
next: { revalidate: 86400 }, // 24 hours - too stale!
});3. Document Cache Strategies
// ✅ GOOD: Document why you chose this strategy
async function getBlogPosts() {
// Cache for 1 hour - blog posts updated ~2-3 times per day
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 },
});
return res.json();
}
async function getUserBalance(userId: string) {
// No cache - critical financial data must be real-time
const res = await fetch(`https://api.example.com/users/${userId}/balance`, {
cache: 'no-store',
});
return res.json();
}4. Consider User Experience
- Visible content: Can tolerate some staleness (5-60 min)
- User actions: Must be fresh (no cache)
- Financial data: Must be real-time (no cache)
- Static assets: Cache aggressively (forever)
5. Monitor Cache Performance
Track cache hit rates and adjust revalidation times based on actual data change patterns.
Key Takeaways
- Default is force-cache - caches indefinitely
- Use revalidate for semi-static - data that changes occasionally
- Use no-store for dynamic - user-specific or real-time data
- Stale-while-revalidate - instant response + background refresh
- Match strategy to data type - different data needs different caching
- Route segment config - set defaults for entire routes
- Document your choices - explain cache strategies in comments
- Balance performance and freshness - cache aggressively but appropriately
What's Next?
You've mastered caching and time-based revalidation! Next, we'll explore Revalidate and Cache Tags—advanced techniques for on-demand revalidation, cache tagging, and programmatic cache invalidation for even more control over your data freshness.
Cache tags let you invalidate specific cached data on-demand (like when content is updated), giving you the best of both worlds: aggressive caching for performance plus the ability to instantly refresh when needed.
⚡ Cache Aggressively
When in doubt, cache more aggressively with revalidation rather than not caching at all. Even 60 seconds of caching dramatically improves performance. Use stale-while-revalidate to get instant responses while keeping data fresh!