Home/Learn/Next.js/Revalidation — Keeping Cached Data Fresh

Revalidation — Keeping Cached Data Fresh

Advanced
Data & Caching

Time-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.

Stale-while-revalidate, shortest interval wins
// 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.

revalidateTag, and a protected webhook
'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.

Per-user is never cached; write the table down
// 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 Aria
1

What is stale-while-revalidate and what does the first visitor after expiry see?

Medium
2

When would you use revalidateTag rather than revalidatePath?

Hard
3

What is the risk of caching a page that shows user-specific content?

Hard

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.

Loading discussion…