Caching and Revalidation in Next.js

WebNext.jsPerformanceArchitecture
Share on LinkedIn

You deployed a blog post editor, published an article, and refreshed the homepage—the old version still appears. The database has the new content. The App Router cached the page at build time and has no idea you changed anything. Next.js 13+ caching is powerful but layered: fetch cache, full route cache, and client router cache each behave differently. Understanding which layer holds your stale data saves hours of confused router.refresh() calls.

The four cache layers

Request → Router Cache (client) → Full Route Cache (server)
                ↓
         Data Cache (fetch) → Origin (DB/API)
Layer Location Duration Invalidation
Request memoization Server (per request) Single render Automatic
Data Cache Server (persistent) Until revalidation revalidateTag, revalidatePath, TTL
Full Route Cache Server (persistent) Until revalidation revalidatePath, rebuild
Router Cache Client (memory) Session / 30s static router.refresh(), navigation

Fetch cache options

// Cached indefinitely (default for static pages)
const posts = await fetch("https://api.example.com/posts");

// Revalidate every 60 seconds (ISR)
const posts = await fetch("https://api.example.com/posts", {
  next: { revalidate: 60 },
});

// Never cache
const user = await fetch("https://api.example.com/me", {
  cache: "no-store",
});

// Tag for on-demand revalidation
const post = await fetch(`https://api.example.com/posts/${slug}`, {
  next: { tags: [`post-${slug}`] },
});

In Server Components, identical fetch calls in one render are deduplicated automatically.

On-demand revalidation

After a Server Action or webhook:

import { revalidatePath, revalidateTag } from "next/cache";

export async function publishPost(slug: string) {
  await db.post.update({ where: { slug }, data: { published: true } });
  revalidateTag(`post-${slug}`);
  revalidatePath("/blog");
  revalidatePath(`/blog/${slug}`);
}

From an external webhook (Route Handler):

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const secret = request.nextUrl.searchParams.get("secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: "Invalid" }, { status: 401 });
  }
  const tag = request.nextUrl.searchParams.get("tag");
  if (tag) revalidateTag(tag);
  return Response.json({ revalidated: true });
}

Route segment config

// app/dashboard/page.tsx
export const dynamic = "force-dynamic";     // never cache this page
export const revalidate = 0;                // same effect
export const fetchCache = "force-no-store"; // all fetches uncached
// app/blog/page.tsx
export const revalidate = 3600; // ISR: rebuild at most once per hour

dynamic = 'force-static' pins the page at build time regardless of fetch options.

Debugging stale data

Add logging to identify which layer serves stale content:

const res = await fetch(url, { next: { revalidate: 60, tags: ["posts"] } });
console.log("cache status:", res.headers.get("x-nextjs-cache")); // HIT, MISS, STALE
Symptom Likely layer Fix
Stale after client navigation Router cache router.refresh()
Stale after hard refresh Data or full route cache revalidatePath
Stale only in production Build-time static generation Check dynamic export
Fresh in dev, stale in prod Expected—dev disables most caching Test with next build && next start

Cache composition example

A product page fetches product data and reviews:

// app/products/[id]/page.tsx
export const revalidate = 300; // 5-minute ISR

async function getProduct(id: string) {
  return fetch(`${API}/products/${id}`, {
    next: { tags: [`product-${id}`] },
  }).then((r) => r.json());
}

async function getReviews(id: string) {
  return fetch(`${API}/products/${id}/reviews`, {
    next: { tags: [`reviews-${id}`], revalidate: 60 },
  }).then((r) => r.json());
}

When a new review arrives, revalidateTag('reviews-${id}') updates reviews without rebuilding the entire product page cache.

Opting out strategically

Not everything should be cached:

Over-caching user data is a security issue—two users seeing the same cached /account response is worse than a slow page.

On-demand revalidation patterns

Webhook-driven invalidation from CMS:

// app/api/revalidate/route.ts
import { revalidateTag } from "next/cache";

export async function POST(request: Request) {
  const secret = request.headers.get("x-revalidate-secret");
  if (secret !== process.env.REVALIDATE_SECRET) {
    return Response.json({ error: "Unauthorized" }, { status: 401 });
  }
  const { tag } = await request.json();
  revalidateTag(tag);
  return Response.json({ revalidated: true, tag });
}

CMS publish webhook → POST /api/revalidate with { tag: "blog-post-123" }. Narrow tags prevent cascade invalidation.

Debugging cache in production

// Temporary — log cache status in fetch
const res = await fetch(url, { next: { tags: ["products"] } });
console.log("cache:", res.headers.get("x-nextjs-cache")); // HIT, MISS, STALE

Common fixes:

Test production caching with next build && next start, never next dev.

Pair with Next.js metadata SEO API when cache invalidation must update OG tags simultaneously.

Common production mistakes

Teams get caching revalidation wrong in predictable ways:

Production implementations of caching revalidation fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Resources

Frequently asked questions

Why does my Next.js page show stale data after a database update?

The App Router caches at multiple layers: fetch responses, full route HTML, and client router cache. A database update alone does not invalidate any of them. Call revalidatePath or revalidateTag after mutations, or set cache: 'no-store' on fetches that must always be fresh.

What is the difference between revalidatePath and revalidateTag?

revalidatePath invalidates cached pages tied to a URL path. revalidateTag invalidates fetch requests tagged with next: { tags: ['posts'] }. Use tags when one data source feeds multiple pages.

How do I disable caching for a specific page?

Export const dynamic = 'force-dynamic' in the page or layout, or use cache: 'no-store' on all fetch calls in that route segment. Use sparingly—disabling cache removes static generation benefits.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →