Skip to content

ISR Caching — How ISR Caching Works at the Edge and Origin

DodaTech Updated 2026-06-28 7 min read

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

  1. 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.
  2. Setting cache headers that conflict with ISR. If Cache-Control has private or no-cache, the CDN won't cache ISR pages. Ensure public caching for ISR pages.
  3. 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.
  4. 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.
  5. 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

  1. What are the three caching layers in an ISR architecture?
  2. How do Cache-Control headers interact with ISR revalidation?
  3. What is the purpose of s-maxage vs max-age in caching?
  4. Why is persistent cache important between deployments?
  5. 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

Does ISR cache persist across server restarts?

Not by default. ISR cache is in-memory on Vercel. For persistence across restarts, use a shared cache like Redis or S3.

How long does ISR cache live on Vercel?

Indefinitely until the next revalidation or deployment. Vercel persists ISR cache in a shared layer across serverless function instances.

Can I use Redis for ISR cache?

Yes. Implement a custom cache layer using Redis. Store rendered HTML and check it before calling getStaticProps during revalidation.

How does CDN caching differ from ISR caching?

CDN caching stores HTTP responses at edge locations. ISR caching stores rendered HTML in the origin server. Both serve cached content but at different layers.

What happens to ISR cache during a cold start?

On cold start (no prior cache), the first request renders the page fresh. Subsequent requests use the cached version until revalidation.

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