ISR Caching — How ISR Caching Works at the Edge and Origin
In this tutorial, you will learn about ISR Caching. We cover key concepts, practical examples, and best practices to help you master this topic.
ISR caching spans multiple layers: CDN edge caches serve static HTML, origin servers handle revalidation, and a persistent cache stores rendered pages.
What You'll Learn
By the end of this tutorial, you'll understand the multi-layer caching architecture of ISR, how the CDN edge, origin server, and persistent cache interact, and how to configure caching headers for optimal performance.
Why It Matters
ISR's performance depends entirely on cache configuration. Misconfigured caching leads to stale content, slow responses, or excessive origin load. Understanding the caching layers helps you diagnose performance issues.
Real-World Use
A high-traffic blog uses ISR with a three-layer cache. The CDN edge serves 95% of requests instantly. The origin server handles revalidation for the remaining 5%. The persistent cache stores rendered pages between deployments.
ISR Cache Architecture
graph TD
A[User Request] --> B[CDN Edge Cache
Fastly / Cloudflare]
B --> C{Cache HIT?}
C -->|Yes| D[Serve from
edge cache
~10ms]
C -->|No| E[Origin Server
Next.js]
E --> F{Persistent
Cache HIT?}
F -->|Yes| G[Serve cached
ISR page
~50ms]
F -->|No| H[Rendered fresh
by getStaticProps
~500ms]
H --> I[Store in
persistent cache]
I --> J[Store at
CDN edge]
D --> K[Response to user]
G --> K
H --> K
style B fill:#4a90d9,color:#fff
style E fill:#e67e22,color:#fff
style H fill:#f39c12,color:#fff
Cache Headers Configuration
// next.config.js — Cache header configuration
module.exports = {
async headers() {
return [
// ISR pages — short CDN cache, SWR
{
source: '/blog/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=300'
}
]
},
// Static assets — long cache, immutable
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
},
// API routes — no caching
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'private, no-cache, no-store, must-revalidate'
}
]
},
// Static pages — long cache
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=86400, stale-while-revalidate=604800'
}
]
}
];
}
};
Cache Layer Management
// lib/cache-manager.js — ISR cache management
class ISRCacheManager {
constructor() {
this.cache = new Map();
this.stats = {
hits: 0,
misses: 0,
revalidations: 0
};
}
// Check if a cached page is fresh
get(path) {
const entry = this.cache.get(path);
if (!entry) {
this.stats.misses++;
return null;
}
const age = Date.now() - entry.generatedAt;
const isFresh = age < entry.revalidate * 1000;
if (isFresh) {
this.stats.hits++;
return entry.html;
}
// Stale — return it but trigger revalidation
this.stats.revalidations++;
this.triggerRevalidation(path, entry);
return entry.html;
}
// Store a rendered page
set(path, html, revalidate) {
this.cache.set(path, {
html,
generatedAt: Date.now(),
revalidate: revalidate || 300
});
}
// Trigger background revalidation
async triggerRevalidation(path, oldEntry) {
if (this.pendingRevalidations?.has(path)) {
return; // Already revalidating
}
if (!this.pendingRevalidations) {
this.pendingRevalidations = new Set();
}
this.pendingRevalidations.add(path);
try {
const freshHtml = await renderPage(path);
this.set(path, freshHtml, oldEntry.revalidate);
console.log(`Cache revalidated: ${path}`);
} catch (err) {
console.error(`Revalidation failed: ${path}`, err);
// Keep stale entry — next request will try again
} finally {
this.pendingRevalidations.delete(path);
}
}
// Invalidate specific paths
invalidate(paths) {
paths.forEach(path => {
this.cache.delete(path);
console.log(`Cache invalidated: ${path}`);
});
}
// Cache statistics
getStats() {
return {
...this.stats,
size: this.cache.size,
hitRate: this.stats.hits / (this.stats.hits + this.stats.misses) * 100
};
}
}
export const cacheManager = new ISRCacheManager();
Persistent Cache Storage
// lib/cache-storage.js — File-based persistent cache
const fs = require('fs');
const path = require('path');
const CACHE_DIR = './.isr-cache';
class PersistentCache {
constructor() {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
}
getCachePath(pathname) {
const safe = pathname.replace(/\//g, '_').replace(/^_/, '');
return path.join(CACHE_DIR, `${safe}.json`);
}
get(pathname) {
try {
const filePath = this.getCachePath(pathname);
if (!fs.existsSync(filePath)) return null;
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
// Check if expired
const age = Date.now() - data.timestamp;
if (age > data.revalidate * 1000) {
return { html: data.html, stale: true };
}
return { html: data.html, stale: false };
} catch {
return null;
}
}
set(pathname, html, revalidate = 300) {
try {
const filePath = this.getCachePath(pathname);
const data = {
html,
timestamp: Date.now(),
revalidate,
path: pathname
};
fs.writeFileSync(filePath, JSON.stringify(data));
return true;
} catch (err) {
console.error(`Cache write failed for ${pathname}:`, err);
return false;
}
}
delete(pathname) {
try {
const filePath = this.getCachePath(pathname);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
}
return false;
} catch (err) {
console.error(`Cache delete failed for ${pathname}:`, err);
return false;
}
}
clear() {
try {
const files = fs.readdirSync(CACHE_DIR);
files.forEach(f => fs.unlinkSync(path.join(CACHE_DIR, f)));
console.log(`Cleared ${files.length} cache entries`);
} catch (err) {
console.error('Cache clear failed:', err);
}
}
getStats() {
try {
const files = fs.readdirSync(CACHE_DIR);
let totalSize = 0;
files.forEach(f => {
totalSize += fs.statSync(path.join(CACHE_DIR, f)).size;
});
return {
entries: files.length,
totalSizeBytes: totalSize,
totalSizeKB: (totalSize / 1024).toFixed(1)
};
} catch {
return { entries: 0, totalSizeBytes: 0 };
}
}
}
export const persistentCache = new PersistentCache();
Cache Invalidation Strategies
// lib/cache-strategies.js — Cache invalidation strategies
const cacheStrategies = {
// Strategy 1: Time-based (default ISR)
timeBased: {
description: 'Revalidate after a fixed time window',
implementation: 'Set revalidate property in getStaticProps',
whenToUse: 'Content changes predictably',
example: 'News site — revalidate every 60 seconds'
},
// Strategy 2: On-demand (webhook-triggered)
onDemand: {
description: 'Revalidate when content changes',
implementation: 'CMS webhook calls res.revalidate()',
whenToUse: 'Content changes unpredictably',
example: 'Blog — revalidate when editor publishes'
},
// Strategy 3: Stale-while-revalidate
staleWhileRevalidate: {
description: 'Serve stale, refresh in background',
implementation: 'Set Cache-Control: stale-while-revalidate',
whenToUse: 'Freshness is desirable but not critical',
example: 'Product listing — show cached while fetching update'
},
// Strategy 4: Hybrid
hybrid: {
description: 'Time-based + on-demand together',
implementation: 'revalidate property + webhook',
whenToUse: 'Need both automatic and instant updates',
example: 'E-commerce — time-based for prices, on-demand for stock'
}
};
Common Mistakes
- Not understanding the difference between CDN cache and ISR cache. The CDN edge caches HTTP responses. ISR cache stores rendered HTML in the Next.js server. They serve different purposes.
- Setting cache headers that conflict with ISR. If Cache-Control has
privateorno-cache, the CDN won't cache ISR pages. Ensure public caching for ISR pages. - Ignoring the persistent cache between deployments. Without a persistent cache, every deployment invalidates all cached ISR pages. Use shared storage (Redis, S3) for persistent caching.
- Not monitoring cache hit rates. A low CDN cache hit rate means the origin is handling too many requests. Check CDN analytics and adjust caching headers.
- Caching user-specific content in the CDN. ISR is for public content. Don't cache authenticated pages at the CDN level — the wrong user might see personalized content.
Practice Questions
- What are the three caching layers in an ISR architecture?
- How do Cache-Control headers interact with ISR revalidation?
- What is the purpose of s-maxage vs max-age in caching?
- Why is persistent cache important between deployments?
- How do you monitor cache effectiveness for ISR pages?
Challenge: Build a cache monitoring dashboard: implement a cache manager that tracks hits, misses, and revalidations, add a persistent cache layer with file storage, configure CDN cache headers for ISR pages, and display real-time cache hit rates.
FAQ
Mini Project
Build a multi-layer cache system for ISR: implement an in-memory cache manager, add a file-based persistent cache layer, configure CDN Cache-Control headers for ISR pages, create a cache monitoring dashboard, and test cache behavior across server restarts.
What's Next
You understand ISR caching. Now learn about ISR Fallback strategies for handling uncached pages.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro