ISR Patterns — Common ISR Patterns and Best Practices
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
- Using a single revalidate value for all pages. Different content types need different freshness levels. Tier your revalidation values.
- 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.
- Over-using ISR when SSG suffices. Static pages that never change don't need ISR. SSG is simpler and caches forever.
- Forgetting that ISR has no built-in retry. Failed revalidations aren't retried. Implement retry queues for critical content.
- Not monitoring revalidation health. Silent failures leave stale content. Set up monitoring dashboards for revalidation rates and error rates.
Practice Questions
- What is tiered revalidation and when should you use it?
- How does staggered revalidation prevent server overload?
- When would you use webhook-only ISR instead of time-based ISR?
- How do you combine SSG, ISR, and SSR in a single application?
- 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
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