Skip to content

ISR Patterns — Common ISR Patterns and Best Practices

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about ISR Patterns. We cover key concepts, practical examples, and best practices to help you master this topic.

Common ISR patterns include tiered revalidation, staggered updates, Webhook-only mode, and hybrid SSG+ISR architectures for production applications.

What You'll Learn

By the end of this tutorial, you'll understand proven ISR patterns used in production, including tiered revalidation, webhook-only ISR, stale-while-revalidate optimization, and incremental adoption from SSG.

Why It Matters

ISR is powerful but easy to misuse. These patterns have been battle-tested in production by teams at Vercel, Next.js, and large-scale content sites. Following them prevents common pitfalls.

Real-World Use

A large e-commerce site uses tiered ISR: product pages revalidate every 300 seconds (prices), inventory pages every 30 seconds (stock levels), and category pages use webhook-only ISR (manual updates). The combination balances server load with freshness needs.

ISR Pattern Architecture

graph TD
    A[ISR Patterns] --> B[Tiered Revalidation]
    A --> C[Webhook-Only ISR]
    A --> D[Hybrid SSG+ISR]
    A --> E[Staggered Revalidation]
    B --> F[Different revalidate
values per page type] C --> G[No time-based revalidate
Webhooks only] D --> H[SSG for stable pages
ISR for dynamic pages] E --> I[Spread revalidation
across time] style B fill:#4a90d9,color:#fff style C fill:#e67e22,color:#fff style D fill:#27ae60,color:#fff style E fill:#3498db,color:#fff

Pattern 1: Tiered Revalidation

// pages/products/[id].js — Tiered revalidation
export async function getStaticProps({ params }) {
    const product = await fetchProduct(params.id);

    // Tier 1: Price-sensitive pages - refresh often
    const tiers = {
        'flash-sale': { revalidate: 10, label: 'Every 10 seconds' },
        'standard': { revalidate: 300, label: 'Every 5 minutes' },
        'evergreen': { revalidate: 86400, label: 'Daily' }
    };

    // Determine tier based on product metadata
    const tier = product.isFlashSale
        ? 'flash-sale'
        : product.isEvergreen
            ? 'evergreen'
            : 'standard';

    return {
        props: {
            product,
            tier: tiers[tier],
            generatedAt: Date.now()
        },
        revalidate: tiers[tier].revalidate
    };
}

// pages/products/index.js — Tiered listing
export async function getStaticProps() {
    const products = await fetchProducts();

    // Group by tier for dashboard display
    const tierGroups = {
        'flash-sale': products.filter(p => p.isFlashSale),
        'standard': products.filter(p => !p.isFlashSale && !p.isEvergreen),
        'evergreen': products.filter(p => p.isEvergreen)
    };

    return {
        props: {
            products,
            tierCounts: {
                'flash-sale': tierGroups['flash-sale'].length,
                'standard': tierGroups['standard'].length,
                'evergreen': tierGroups['evergreen'].length
            }
        },
        // Listing revalidates at the fastest tier rate
        revalidate: 10
    };
}

Pattern 2: Webhook-Only ISR

// pages/about.js — Webhook-only ISR
export async function getStaticProps() {
    const page = await fetchPage('about');

    return {
        props: { page },
        // No revalidate — only rebuilds via webhook
        revalidate: false
    };
}

// pages/api/revalidate.js — Webhook handler
// Manually triggers revalidation for all webhook-only pages
export default async function handler(req, res) {
    if (req.query.secret !== process.env.REVALIDATION_TOKEN) {
        return res.status(401).json({ message: 'Invalid token' });
    }

    const paths = [
        '/about',
        '/contact',
        '/faq',
        '/terms',
        '/privacy'
    ];

    try {
        await Promise.all(paths.map(path => res.revalidate(path)));
        res.json({ revalidated: true, paths });
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
}

// This pattern is ideal for:
// - Marketing pages (rarely change)
// - Legal pages (require manual review)
// - Configuration pages (team-controlled)
// Benefits: zero server load between updates

Pattern 3: Staggered Revalidation

// lib/staggered-revalidation.js — Spread revalidation load
class StaggeredRevalidation {
    constructor(totalPages, baseInterval = 60) {
        this.totalPages = totalPages;
        this.baseInterval = baseInterval;
        this.groups = this.createGroups();
    }

    createGroups() {
        // Divide pages into groups that revalidate at different times
        const groupCount = Math.min(this.totalPages, 10);
        const groups = [];

        for (let i = 0; i < groupCount; i++) {
            groups.push({
                groupId: i,
                offset: (this.baseInterval / groupCount) * i,
                interval: this.baseInterval * (i + 1)
            });
        }

        return groups;
    }

    getRevalidateForPage(pageId) {
        // Assign page to group based on ID
        const groupIndex = pageId % this.groups.length;
        return this.groups[groupIndex].interval;
    }

    getGroupForPage(pageId) {
        return this.groups[pageId % this.groups.length];
    }
}

// Usage in getStaticProps
const stagger = new StaggeredRevalidation(1000, 60);

export async function getStaticProps({ params }) {
    const pageId = parseInt(params.id);
    const revalidate = stagger.getRevalidateForPage(pageId);

    return {
        props: {
            data: await fetchData(params.id),
            groupInfo: stagger.getGroupForPage(pageId)
        },
        revalidate
    };
}

Pattern 4: Hybrid SSG + ISR

// pages/marketing/about.js — Pure SSG (no revalidation)
export async function getStaticProps() {
    const content = await fetchMarketingContent('about');

    return {
        props: { content }
        // No revalidate = fully static
    };
}

// pages/blog/[slug].js — ISR (regular updates)
export async function getStaticProps({ params }) {
    const post = await fetchPost(params.slug);

    return {
        props: { post },
        revalidate: 300
    };
}

// pages/dashboard.js — SSR (user-specific)
export async function getServerSideProps(context) {
    const userId = context.req.session?.userId;
    if (!userId) {
        return { redirect: { destination: '/login' } };
    }

    const dashboardData = await fetchUserDashboard(userId);
    return { props: { dashboardData } };
}

// This hybrid approach gives you:
// - Marketing pages: Fastest (SSG, CDN-cached forever)
// - Blog pages: Fast + fresh (ISR, periodic updates)
// - Dashboard: Dynamic (SSR, user-specific)

Pattern 5: ISR with Queue

// lib/revalidation-queue.js — Queue-based revalidation
class RevalidationQueue {
    constructor() {
        this.queue = [];
        this.processing = false;
        this.maxConcurrent = 5;
    }

    add(path) {
        if (!this.queue.includes(path)) {
            this.queue.push(path);
            this.process();
        }
    }

    addBatch(paths) {
        paths.forEach(path => {
            if (!this.queue.includes(path)) {
                this.queue.push(path);
            }
        });
        this.process();
    }

    async process() {
        if (this.processing || this.queue.length === 0) return;

        this.processing = true;

        while (this.queue.length > 0) {
            const batch = this.queue.splice(0, this.maxConcurrent);

            const results = await Promise.allSettled(
                batch.map(async (path) => {
                    const start = Date.now();
                    try {
                        await res.revalidate(path); // Note: res needed from context
                        return {
                            path,
                            status: 'success',
                            duration: Date.now() - start
                        };
                    } catch (err) {
                        return {
                            path,
                            status: 'failed',
                            error: err.message,
                            duration: Date.now() - start
                        };
                    }
                })
            );

            const succeeded = results.filter(r => r.value?.status === 'success');

            if (succeeded.length > 0) {
                console.log(`Revalidated ${succeeded.length} paths`);
            }
        }

        this.processing = false;
    }

    getQueueLength() {
        return this.queue.length;
    }
}

export const revalidationQueue = new RevalidationQueue();

Common Mistakes

  1. Using a single revalidate value for all pages. Different content types need different freshness levels. Tier your revalidation values.
  2. Not staggering revalidation times. If all 10,000 pages revalidate every 60 seconds at the same time, your server gets 167 requests/second for revalidation. Stagger them.
  3. Over-using ISR when SSG suffices. Static pages that never change don't need ISR. SSG is simpler and caches forever.
  4. Forgetting that ISR has no built-in retry. Failed revalidations aren't retried. Implement retry queues for critical content.
  5. Not monitoring revalidation health. Silent failures leave stale content. Set up monitoring dashboards for revalidation rates and error rates.

Practice Questions

  1. What is tiered revalidation and when should you use it?
  2. How does staggered revalidation prevent server overload?
  3. When would you use webhook-only ISR instead of time-based ISR?
  4. How do you combine SSG, ISR, and SSR in a single application?
  5. What metrics should you monitor for ISR health?

Challenge: Implement all five ISR patterns in a single application: tiered revalidation for different content types, staggered revalidation for a large product catalog, webhook-only ISR for marketing pages, hybrid SSG+ISR+SSR in one Next.js app, and a revalidation queue with monitoring dashboard.

FAQ

Which ISR pattern is most common in production?

Tiered revalidation is the most common. It balances freshness needs with server load by assigning different revalidate values based on content volatility.

How many revalidations per second can my server handle?

This depends on your hosting. Vercel serverless functions scale automatically. A single function can handle ~10 revalidations/second. Use staggering for large-scale needs.

Should I always use ISR over SSG?

No. ISR adds server complexity. Use SSG for truly static content. ISR only when content freshness matters and updates are frequent enough to justify the overhead.

Can I change the ISR pattern without rebuilding?

Yes. ISR patterns are runtime configuration. Changing revalidate values only requires redeploying the code — not a full content rebuild.

How do I debug ISR issues in production?

Add generation timestamps to pages, monitor revalidation API logs, set up alerting for revalidation failures, and use Vercel's ISR debugging tools.

Mini Project

Build a production-ready ISR application: implement all five patterns (tiered, staggered, webhook-only, hybrid, queued), create a monitoring dashboard showing revalidation rates and success rates, and document which pattern to use for each content type.

What's Next

You've learned ISR patterns. Now learn how to set up ISR Monitoring to track revalidation health and performance.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro