Suggest an editImprove this articleRefine the answer for “Revalidation strategies in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Revalidation** in Next.js updates cached pages without a full rebuild. ```tsx fetch(url, { next: { revalidate: 3600 } }); // Serves cache instantly; background fetch runs after 1 hour ``` **Key:** time-based (`revalidate: N`) for periodically changing data; on-demand (`revalidatePath`, `revalidateTag`) for immediate cache invalidation after mutations.Shown above the full answer for quick recall.Answer (EN)Image**Revalidation** in Next.js updates cached pages with fresh data without triggering a full site rebuild. ## Theory ### TL;DR - Think of a coffee shop with pre-brewed pots: customers always get coffee from the pot (cache), but every hour someone brews a fresh batch in the background while the old one is still being served. - Time-based revalidation refreshes on a schedule (stale-while-revalidate); on-demand revalidation invalidates the cache the moment data changes. - Decision rule: time-based for content that changes rarely (blogs, docs); on-demand for anything where stale data causes real problems (prices, inventory). - `revalidateTag` gives surgical control: one tag can cover multiple fetches across different routes. ### Quick example ```tsx // app/products/page.tsx (App Router) async function getProducts() { const res = await fetch('https://api.example.com/products', { next: { revalidate: 3600 } // Serve cache for 1 hour, then refresh in background }); return res.json(); } export default async function ProductsPage() { const products = await getProducts(); // First visit: fetches live data, stores in cache // Visits within 1 hour: served from cache (~50ms) // First visit after 1 hour: still gets cached version, // but triggers a background fetch for the next visitor return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>; } ``` That last point is the one most developers miss. The user who triggers revalidation still gets stale data. Only the *next* visitor gets the fresh version. ### Key difference Time-based and on-demand revalidation solve different problems. Time-based works with stale-while-revalidate: the page is served from cache instantly, and Next.js spawns a background job to regenerate it after the TTL expires. On-demand skips the timer entirely. A webhook or server action calls `revalidatePath()` or `revalidateTag()`, and the cache is invalidated right away. No waiting for a timer to run out. ### When to use - Blog posts, docs, SEO landing pages: `revalidate: 86400` (daily). A day-old article is fine. - E-commerce product catalog: `revalidate: 300` (5 minutes) as a fallback, plus `revalidateTag('products')` from a Shopify/Stripe webhook for immediate updates. - Live scores, exchange rates, stock prices: `cache: 'no-store'` or `export const dynamic = 'force-dynamic'`. Do not cache at all. - User-specific data (dashboards, profiles): `noStore()` or per-request dynamic rendering. ISR does not work here. - Large sites with related content (cart + product list): tag-based revalidation with `revalidateTag('cart')` so only what changed gets updated. ### Comparison table | Strategy | Trigger | Stale tolerance | Best for | Latency impact | |---|---|---|---|---| | Time-based (ISR) | Timer (`revalidate: 3600`) | Up to interval | Blogs, SEO pages | None (stale-while-revalidate) | | On-demand path | `revalidatePath('/products')` | Zero after trigger | CMS content, mutations | None (background) | | On-demand tag | `revalidateTag('products')` | Zero after trigger | E-commerce, granular updates | None (background) | | Per-request dynamic | Every request | None | Dashboards, user data | High (always fetches) | | No cache | `cache: 'no-store'` | None | Real-time data | High (always fetches) | | When to use | Low-change data → time-based; urgent updates → on-demand; user data → dynamic | | | | ### How it works internally On the first request, Next.js generates the page and stores the output (HTML + JSON) in `.next/cache` on disk, or in Vercel's distributed cache. Subsequent requests check whether the TTL has expired. If not, the cached response goes out immediately. If it has expired, the stale response still goes out immediately (that is stale-while-revalidate), but Next.js spawns a background Node.js worker to re-fetch data and regenerate the page. Once done, it atomically swaps the cache entry so the next request gets the fresh version. On-demand revalidation skips the TTL check entirely. `revalidatePath` or `revalidateTag` marks specific cache entries as stale on the server. The next request to that path triggers a fresh fetch, not a scheduled one. One edge case worth knowing: the Router Cache (in-memory cache of page shells in the browser) and the Data Cache (server-side) are separate layers. `revalidatePath('/products')` invalidates the Data Cache, but the Router Cache in the browser can persist for up to 30 seconds in Next.js 14. If the layout also needs to update, use `revalidatePath('/', 'layout')`. ### Common mistakes **1. Mixing `revalidate` with dynamic rendering triggers** ```tsx // Wrong: searchParams forces dynamic rendering, revalidate is ignored export const revalidate = 3600; export default function Page({ searchParams }: any) { // searchParams opts this page into dynamic rendering // revalidate does nothing here const query = searchParams.q; return <Results query={query} />; } ``` In Next.js 14+, `searchParams` switches the page to dynamic rendering. The `revalidate` export gets ignored. Either remove `revalidate` and accept dynamic behavior, or add `noStore()` explicitly. **2. Assuming revalidation happens on a schedule when there is no traffic** ```tsx fetch(url, { next: { revalidate: 3600 } }); // If no one visits the page after the TTL expires, // the cache just sits stale indefinitely. // There is no background cron - revalidation requires an incoming request. ``` On low-traffic pages, add a cron job that hits the URL periodically, or switch to on-demand revalidation via webhook. **3. Router Cache vs Data Cache confusion** ```tsx // After revalidatePath('/'), the Data Cache updates. // But the Router Cache (browser-side page shell) may still be stale. // Fix: revalidatePath('/', 'layout'); // Propagates through all layout segments ``` **4. Calling `revalidateTag` on untagged fetches** ```tsx // This fetch has no tag - revalidateTag will not touch it fetch(url, { next: { revalidate: 60 } }); // Later: revalidateTag('products'); // Does nothing for the fetch above ``` Always add `{ tags: ['products'] }` to fetches you want to control with `revalidateTag`. Time-based and tag-based options are mutually exclusive per fetch call. ### Real-world usage - **Vercel Commerce (Next.js Commerce):** on-demand webhooks from Shopify/Stripe trigger `revalidateTag('products')` on inventory changes. - **TinaCMS:** time-based ISR with `revalidate: 10` seconds for markdown content, so edits appear almost instantly without redeploys. - **Supabase + Next.js:** tag-based revalidation like `` revalidateTag(`user-${id}`) `` after authentication state changes. - **Medusa.js:** hybrid approach - time-based for product catalogs, on-demand for order status. - As a comparison point: use revalidation over `dynamic` rendering when the data is semi-static and traffic is high. The difference between a cached response (under 50ms) and a dynamic fetch (200-500ms+) is significant at scale. ### Follow-up questions **Q:** What is the difference between stale-while-revalidate in Next.js and the `stale-while-revalidate` HTTP Cache-Control directive? **A:** In Next.js, stale-while-revalidate is a server-side behavior: Next.js serves the cached page and regenerates it in the background using a Node.js worker. The HTTP directive does the same thing but at the CDN or browser level (Cloudflare, for example). They can work together - Next.js regenerates the server cache while the CDN serves its own stale copy. **Q:** How does revalidation differ between the App Router and the Pages Router? **A:** The Pages Router has a single ISR entry per path with no tag support. The App Router introduces separate Data Cache and Router Cache layers, supports `revalidateTag` for granular invalidation across multiple routes, and lets you revalidate at the layout segment level. **Q:** What happens if `revalidatePath` is called during a Server Action? **A:** Next.js marks the path's cache entry as stale immediately. The revalidation happens after the Server Action completes, not during it. The response from the action goes out first, then the cache is purged. **Q:** (Senior) In a nested layout with shared data across segments, how do you update the cart count in the header without re-fetching everything? **A:** Tag the cart fetch in the layout component with `{ tags: ['cart'] }`. After a cart mutation in a Server Action or webhook, call `revalidateTag('cart')`. Only the tagged fetches regenerate - the rest of the layout stays cached. This beats `revalidatePath('/', 'layout')`, which invalidates everything at once. ## Examples ### Basic: time-based revalidation for a blog ```tsx // app/blog/page.tsx async function getPosts() { const res = await fetch('https://cms.example.com/posts', { next: { revalidate: 86400 } // Refresh once per day }); return res.json(); } export default async function BlogPage() { const posts = await getPosts(); return ( <ul> {posts.map((post: { id: string; title: string }) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); } ``` A blog post that is a day old is fine. The page loads in under 50ms from cache, and readers never notice the background regeneration. ### Intermediate: on-demand revalidation with a Stripe webhook ```tsx // app/shop/products/page.tsx async function getProducts() { const res = await fetch('https://api.stripe.com/v1/products?limit=10', { next: { tags: ['featured-products'] } // No timer - only updates when the webhook fires }); return res.json(); } export default async function ProductsPage() { const { data: products } = await getProducts(); return products.map((p: { id: string; name: string }) => ( <div key={p.id}>{p.name}</div> )); } ``` ```tsx // app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; export async function POST(request: Request) { // In production: verify the Stripe webhook signature here revalidateTag('featured-products'); return Response.json({ revalidated: true, timestamp: Date.now() }); } ``` When Stripe sends a `product.updated` event, the webhook hits `/api/revalidate`, the tag is invalidated, and the next visitor to `/shop/products` gets fresh data. Everyone before that still gets the cached version. That is the intended behavior. ### Advanced: Server Action with tag-based revalidation ```tsx // actions/posts.ts 'use server'; import { revalidateTag } from 'next/cache'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; await db.insert(posts).values({ title }); // Invalidates all fetches tagged 'posts' across all routes revalidateTag('posts'); } ``` ```tsx // app/blog/new/page.tsx 'use client'; import { createPost } from '@/actions/posts'; export function NewPostForm() { return ( <form action={createPost}> <input name="title" placeholder="Post title" required /> <button type="submit">Publish</button> </form> ); } ``` After submit, `revalidateTag('posts')` fires and every route fetching with `{ tags: ['posts'] }` gets fresh data on the next request. No separate API route needed - Server Actions handle the revalidation directly.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.