Revalidation — Keeping Cached Data Fresh
AdvancedTime-based revalidation is a guess about how stale you can afford to be. On-demand revalidation is knowing exactly when something changed — and it is almost always the better answer.
Overview
Caching is only half a design; the other half is deciding when a cached value stops being true. Next gives two mechanisms. Time-based revalidation regenerates a page every N seconds, which is simple and always slightly wrong — you either refresh too often or serve stale content. On-demand revalidation fires when the underlying data actually changes, which is precise, and is the right default whenever you control the write path. Tags make the second practical by letting one write invalidate exactly the pages that depended on it.
Time-Based
The route-level and fetch-level forms, and what the user actually sees.
// Whole route
export const revalidate = 3600 // seconds
// One fetch
fetch(url, { next: { revalidate: 60 } })
// The route uses the SHORTEST revalidate of anything inside it.
// The behaviour is stale-while-revalidate, which surprises people:
// request at t=0 -> generated, cached
// request at t=30 -> served from cache (fast)
// request at t=70 -> STALE COPY SERVED, regeneration starts
// request at t=71 -> fresh copy
// So the first visitor after expiry still sees the old page. Nobody
// waits for a rebuild, and nobody is guaranteed the newest content.
// Choose the interval from how wrong you can afford to be:
// a published article 3600 or more
// a problem list 300
// a leaderboard 30, or on-demand
// a price, a balance never cache
// revalidate = 0 means "always dynamic" and is the same as
// force-dynamic — not "revalidate immediately".On-Demand
Invalidate at the moment of the write. Path or tag.
'use server'
import { revalidatePath, revalidateTag } from 'next/cache'
export async function publishProblem(id: string) {
const problem = await db.problem.update({ where: { id }, data: { published: true } })
revalidatePath('/problems') // the list
revalidatePath(`/problems/${problem.slug}`) // this page
revalidateTag('problems') // everything tagged
}
// Tags are the scalable version. Tag the fetch:
fetch(`${API}/problems`, { next: { tags: ['problems'] } })
fetch(`${API}/problems/${slug}`, { next: { tags: ['problems', `problem:${slug}`] } })
// Then one write invalidates exactly what it should:
revalidateTag(`problem:${slug}`) // narrow
revalidateTag('problems') // broad
// revalidatePath with a dynamic segment needs the type:
revalidatePath('/problems/[slug]', 'page') // all of them
revalidatePath('/(app)', 'layout') // a whole subtree
// From outside the app — a CMS webhook, or your FastAPI backend
// after it changes data Next has cached:
// app/api/revalidate/route.ts
export async function POST(req: NextRequest) {
if (req.headers.get('x-secret') !== process.env.REVALIDATE_SECRET)
return new Response('Unauthorized', { status: 401 })
revalidateTag((await req.json()).tag)
return Response.json({ revalidated: true })
}
// Protect it. An open revalidation endpoint is a cheap way for
// someone to make you regenerate every page continuously.Designing the Strategy
Per data type, written down once rather than decided per endpoint.
// Write it as a table for the project, not per file:
//
// data strategy why
// ────────────────────────────────────────────────────────────────
// concept pages static + on-demand changes on publish
// problem list tag 'problems' changes on admin write
// user dashboard dynamic, no-store per user, must be live
// subscription status dynamic, no-store money; never stale
// leaderboard revalidate 60 approximate is fine
// blog revalidate 3600 rarely changes
// Two rules that prevent most incidents:
// 1. Anything per-user is dynamic. A cached page keyed by URL alone
// WILL be served to the wrong user. This is the worst caching
// bug there is, and it is entirely preventable.
// 2. Anything about money or permissions is never cached.
// After every mutation, list what it invalidated — in a comment next
// to the action, so a reviewer can check it:
// invalidates: /problems, problem:{slug}, /dashboard
// A missed invalidation shows as "I saved it but it did not change",
// which users report as data loss rather than as a caching bug.Key Points to Remember
- 1Time-based revalidation serves a stale copy to the first visitor after expiry and regenerates in the background
- 2A route revalidates at the shortest interval of anything inside it
- 3On-demand revalidation with tags invalidates exactly the pages a write affected and is the better default
- 4An external revalidation endpoint must be authenticated, or anyone can force continuous regeneration
- 5Never cache per-user data — a cache keyed by URL alone will eventually serve one user another user's page
Interview Questions
Sign in to ask AriaWhat is stale-while-revalidate and what does the first visitor after expiry see?
When would you use revalidateTag rather than revalidatePath?
What is the risk of caching a page that shows user-specific content?
Ask Aria about Revalidation — Keeping Cached Data Fresh
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.