# How to move from time-based to on-demand revalidation

**Author:** Jonas Herrmannsdörfer, Mark Knichel

---

Time-based revalidation can regenerate routes even when their data hasn't changed. If your source system can emit a reliable change event, use that event to revalidate only the affected paths and cache tags.

This guide uses the Next.js 16 App Router. It covers external change events and mutations inside your application. If you haven't identified which routes or tags drive your usage, start with [How To Reduce ISR Revalidation Costs](https://vercel.com/kb/guide/how-to-reduce-isr-revalidation-costs).

## Decide whether on-demand revalidation fits

On-demand revalidation works well when your application knows when data changes. The event can come from any source of truth, including a content platform, commerce system, inventory service, or database.

Use it when:

- Your source can send a reliable event after a publish, update, unpublish, or delete.
  
- A mutation happens inside your Next.js application.
  
- You can map each event to the paths and data that depend on it.
  

Keep time-based revalidation when you can't observe changes at the source. For example, a third-party feed that offers no webhook or change stream still needs polling. Next.js documents both approaches in its [ISR guide](https://nextjs.org/docs/app/guides/incremental-static-regeneration).

This guide doesn't add a time-based fallback. That keeps regeneration tied to real changes, but it also makes reliable event delivery part of your application design.

## Choose a path or a tag

Next.js provides two scoped on-demand APIs:

- [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) invalidates one page, a page pattern, or a layout subtree.
  
- [`revalidateTag`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag) marks data with a matching cache tag as stale across every route that uses it.
  

Use `revalidatePath` when one record maps cleanly to one page. Use tags when the same data appears on several routes.

Without Cache Components, tags belong to individual cached `fetch` requests, not pages. For example, a product page can tag the request that loads product `123`:

`const product = await fetch('<https://api.example.com/products/123>', { next: { tags: ['product:123'] }, })` Calling `revalidateTag('product:123', 'max')` marks that cached API response as stale. The next visit to any route that uses it gets refreshed data. It does not directly target `/products/red-shoe`. Instead, the next visit to any route that reads that tagged data can serve the stale value while Next.js refreshes it in the background. If only that product page depends on the change, use `revalidatePath('/products/red-shoe')` instead. You don't need to add a tagged `fetch` or restructure the route just to invalidate one known path. With [Cache Components](https://nextjs.org/docs/app/getting-started/cache-components), use [`cacheTag`](https://nextjs.org/docs/app/api-reference/functions/cacheTag) inside a `'use cache'` scope:

``import { cacheTag } from 'next/cache' export async function Products({ locale }: { locale: string }) { 'use cache' // A product change can invalidate only the list for this locale. cacheTag(`products:list:${locale}`) const products = await getProductsFromDatabase(locale) return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ) }``

For a cached `fetch` without Cache Components, attach tags to the request:

``type Product = { id: string name: string } export async function Products({ locale }: { locale: string }) { const response = await fetch( `https://api.example.com/products?locale=${locale}`, { next: { tags: [`products:list:${locale}`] }, }, ) if (!response.ok) { throw new Error('Failed to load products') } const products = (await response.json()) as Product[] return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ) }`` ## Start with the smallest safe scope Map each change to the smallest scope that contains every affected view: 1. **One path:** `/products/red-shoe`     2. **One content tag:** `product:123`     3. **One route-group tag:** `category:shoes:en`     4. **One shared-content tag:** `navigation:en`     5. **Whole-site invalidation:** only when every cached route depends on the change     Useful tag patterns include: - `post:${slug}`    - `product:${id}`    - `category:${slug}:${locale}`    - `posts:list:${locale}`    - `navigation:${locale}`    - `footer:${locale}`    Include the locale or market when the underlying data differs by locale or market. This prevents one regional update from invalidating unrelated variants. Be precise with root paths. `revalidatePath('/')` targets the root page. `revalidatePath('/', 'layout')` invalidates the root layout, every layout below it, and every page below those layouts. Next.js documents this as a way to [revalidate all data](https://nextjs.org/docs/app/api-reference/functions/revalidatePath#revalidating-all-data), so don't use it for a local change.

## Handle external change events

An external system should send a business event, not arbitrary paths or tags. Your application owns the mapping from the event to cache entries. This keeps the invalidation surface allowlisted and makes its blast radius reviewable.

The example below accepts a bounded batch. It deduplicates paths and tags so a bulk update invalidates each target once.

``import { revalidatePath, revalidateTag } from 'next/cache' const MAX_CHANGES_PER_REQUEST = 100 type Change = | { type: 'product.published' | 'product.updated' id: string slug: string locale: string } | { type: 'product.unpublished' id: string previousSlug: string locale: string } | { type: 'category.updated' slug: string locale: string } type ChangeBatch = { eventId: string changes: Change[] } type Invalidation = { paths: string[] tags: string[] } function mapChangeToInvalidation(change: Change): Invalidation { switch (change.type) { case 'product.published': case 'product.updated': return { // A product maps cleanly to its detail page. paths: [`/${change.locale}/products/${change.slug}`], // Lists and related views can share these tagged records. tags: [ `product:${change.id}`, `products:list:${change.locale}`, ], } case 'product.unpublished': return { paths: [`/${change.locale}/products/${change.previousSlug}`], tags: [ `product:${change.id}`, `products:list:${change.locale}`, ], } case 'category.updated': return { paths: [], tags: [`category:${change.slug}:${change.locale}`], } } } export async function POST(request: Request) { // Verify a provider signature or bearer credential before parsing the event. await authenticateChangeEvent(request) // Validate allowed event types, required fields, locales, and the batch limit. const payload = await validatePayload<ChangeBatch>(request, { maxChanges: MAX_CHANGES_PER_REQUEST, }) const paths = new Set<string>() const tags = new Set<string>() for (const change of payload.changes) { const invalidation = mapChangeToInvalidation(change) invalidation.paths.forEach((path) => paths.add(path)) invalidation.tags.forEach((tag) => tags.add(tag)) } // Route Handlers mark paths for regeneration on their next visit. paths.forEach((path) => revalidatePath(path)) // "max" serves stale data while the next visit refreshes it in the background. tags.forEach((tag) => revalidateTag(tag, 'max')) return Response.json({ accepted: payload.changes.length, invalidatedPaths: paths.size, invalidatedTags: tags.size, }) }`` `authenticateChangeEvent()` and `validatePayload()` are application-specific pseudocode helpers. Authentication should verify the sender before the handler acts. Use signed request verification when the event provider supports it. If you use a shared secret, store it in an environment variable and compare it safely. Validation should reject unknown event types, missing identifiers, unsupported locales, and oversized batches. [`revalidateTag(tag, 'max')`](https://nextjs.org/docs/app/api-reference/functions/revalidateTag#revalidation-behavior) marks tagged data as stale. The next visit serves the stale value while fresh data loads in the background. It doesn't immediately regenerate every route that uses the tag.

Use immediate expiration only when stale data can't be served, such as an urgent legal or security removal. Next.js supports `revalidateTag(tag, { expire: 0 })` for external systems that require this behavior. Keep `'max'` for normal updates because it avoids a blocking cache miss.

A Route Handler call to [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath#usage) also waits until the next visit to regenerate the path. This limits work after a broad event, but a broad mapping can still cause many later regenerations, reads, Function invocations, and changed outputs.

## Map every event that changes visible data

A reliable event mapping normally covers:

- **Publish:** invalidate the new detail page and every listing or relationship that now includes it.
  
- **Update:** invalidate the record and the listings, categories, or related views that display changed fields.
  
- **Unpublish or delete:** invalidate the previous path and remove the record from affected listings.
  
- **Referenced records:** map an author, category, price, or inventory change to the pages that read that record.
  
- **Locale-specific updates:** invalidate only paths and tags for the changed locale.
  

Don't send production invalidations for draft or preview-only changes. Next.js [Draft Mode](https://nextjs.org/docs/app/guides/draft-mode) lets preview requests bypass static rendering without changing the published cache.

## Avoid event storms

Deduplicate paths and tags within each batch, as the example does. For duplicate deliveries across requests, send a stable event ID and record which IDs you've processed, or route events through a queue. Keep this layer small and use your existing durable storage rather than adding a revalidation-specific database.

For large imports or bursty update streams, [Vercel Queues](https://vercel.com/docs/queues) can separate event receipt from processing. A queue is useful when you need delivery retries, controlled concurrency, or work that shouldn't run inside one webhook request. For a bulk job with a known completion point, sending one summary event after the job can be simpler.

## Handle mutations inside the application

Use [`updateTag`](https://nextjs.org/docs/app/api-reference/functions/updateTag) in a Server Action when the user who made a change must immediately read the new value. `updateTag` expires tagged data and supports read-your-own-writes behavior. It can only run in a Server Action.

``'use server' import { updateTag } from 'next/cache' export async function updateProductName( id: string, locale: string, name: string, ) { await updateProductInDatabase({ id, name, locale }) // Expire all cached data affected by this mutation. updateTag(`product:${id}`) updateTag(`products:list:${locale}`) }``

Use `revalidateTag(tag, 'max')` when it’s acceptable for a visitor to briefly see the previous value after an update. This usually fits content, product descriptions, navigation labels, and other non-critical data.

Use `updateTag(tag)` when the person who made the change must see it on their next render. For example, after a customer updates a product name in your dashboard, they should see the new name immediately. The [Next.js revalidation guide](https://nextjs.org/docs/app/getting-started/revalidating) compares these APIs.

## Treat shared layout data separately

Navigation and footer data often appear on every page. When you invalidate shared navigation data, every page that includes it needs a new rendered output the next time it is visited. On a site with thousands of ISR pages, one navigation update can therefore trigger thousands of regenerations. That increases Function duration, bandwidth, and reads from your backend.

To avoid regenerating every page for a shared navigation update, keep the navigation in the initial server render, then fetch its latest value from a shared endpoint on the client. Cache that endpoint and revalidate only the endpoint when navigation changes. Visitors get the updated navigation without regenerating each page.

See [Updating large-scale site navigation with minimal revalidation](https://vercel.com/kb/guide/update-mega-nav-min-reval) for the complete pattern.

## Compatibility with classic ISR and the Pages Router

The same scope rules apply without Cache Components:

- In the App Router, attach tags to cached `fetch` requests with `next.tags`, then call `revalidateTag(tag, 'max')` from a Server Action or Route Handler.
  
- If one record maps to one route, call `revalidatePath` instead of introducing a tagged data request only for invalidation.
  
- In the Pages Router, use `res.revalidate(path)` in an authenticated API Route. See the [Pages Router ISR guide](https://nextjs.org/docs/pages/guides/incremental-static-regeneration#on-demand-validation-with-resrevalidate) for the implementation and caveats.
  

## Investigate with an agent

Use this prompt with a coding agent that can read your repository and use the Vercel CLI:

``Audit this Next.js project for unnecessary time-based revalidation. Start read-only. Use the Vercel CLI to confirm the current team and linked project. Run `vercel metrics schema` before querying metrics. Use `vercel metrics` to find routes, paths, deployments, and tags with high revalidation volume. Group `vercel.request.count` by `cache_reason`, then inspect representative requests with `vercel logs --request-id <request-id> --expand --json`. Return the commands used and correlate the results with deployments and source change events. Search the repository for timers, invalidation calls, cache tags, and event endpoints. Include searches equivalent to: - `rg "revalidate\\s*=|next:\\s*\\{[^}]*revalidate" app src` - `rg "revalidatePath|revalidateTag|updateTag" app src` - `rg "cacheTag|cacheLife|next:\\s*\\{[^}]*tags" app src` - `rg "webhook|eventId|published|updated|unpublished|deleted" app src` Find revalidate intervals, cacheLife profiles, revalidatePath calls, revalidateTag and updateTag calls, cacheTag and next.tags usage, and endpoints that receive external change events. For each affected route, map the source data to its paths and tags. Flag timers for event-driven data, broad path or layout invalidation, generic tags, missing locale scope, duplicate event handling, and updates that do not cover dependent list or reference views. Recommend the smallest safe invalidation scope. Separate external events that fit revalidateTag(tag, 'max') from Server Action mutations that need updateTag. Return evidence by file and line. Do not edit files.`` ## Migration checklist 1. Every migrated timer has a reliable source change event.     2. The application, not the caller, maps events to allowlisted paths and tags.     3. One-to-one records use a literal path where that is simpler than tagging data.     4. Shared records use scoped tags across every dependent route.     5. Tag names include locale or market where the data differs.     6. `revalidateTag` uses the current two-argument API with `'max'`.     7. Interactive Server Actions use `updateTag` only when immediate consistency matters.     8. Publish, update, unpublish, delete, and referenced-record events are covered.     9. Draft or preview events don't invalidate published output.     10. Bulk batches deduplicate paths and tags, and reject unbounded input.      11. Root-layout and whole-site invalidation are removed or justified.      12. Revalidation volume, cache reasons, and usage are checked after release.      If frequency and scope are correct but changed pages still use many write units, continue with [How to reduce ISR Writes by shrinking cached output](https://vercel.com/kb/guide/how-to-reduce-isr-writes).

---

[View full KB sitemap](/kb/sitemap.md)
