Caching and Revalidation in Next.js

Share on LinkedIn Share on X Share on Reddit Share on HN Share on Bluesky

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.

Production validation (1)

Ship changes behind feature flags when behavior crosses route or service boundaries. Canary deploy with automatic rollback when error rate or p95 latency regresses beyond SLO budget. Document which metrics prove success—user-visible latency, error ratio, conversion—not only CPU graphs.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Failure modes (2)

Recurring incidents: missing idempotency on retried paths, connection pool exhaustion masquerading as slow queries, retry storms amplifying partial outages. Design explicit timeouts on every outbound call.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Observability (3)

Structured logs include trace_id and tenant_id on every error path. Metrics: request rate, error ratio, duration histogram, queue depth or pool wait. Traces: one span per dependency.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Security review (4)

Least-privilege credentials, no PII in logs, fail-closed auth defaults. Secrets rotate without redeploy where possible. Never log raw tokens or authorization headers.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Testing strategy (5)

Integration tests against real Postgres/Redis in CI with Testcontainers. Load test at 2× peak with production-like payloads. Chaos: inject dependency latency and verify degradation matches runbooks.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Rollout checklist (6)

Staging mirrors production topology for cache, pools, and timeouts. Rollback path tested quarterly. On-call runbook fits one page: symptom, dashboard, mitigation, rollback.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

Performance tuning (7)

Measure p50/p95 before optimizing. Change one variable at a time—pool size, batch size, TTL, timeout. Profile CPU for JSON serialization and regex; profile IO for N+1 and pool wait.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

On-call triage (8)

Confirm scope: one tenant, region, or deploy stage? Check deploys and migrations in last 24h. Compare golden signals to baseline. Rollback first during incident if faster than root cause.

When operating Next.js caching revalidation (nextjs-caching-revalidation), tie this section to a measurable SLI—latency, error rate, freshness, or throughput—and review it in weekly ops until the pattern is boringly stable.

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 →