ProgrUmar Logo
Module 2: React Server Components & Data Fetching

Caching & Revalidation Strategies

Duration: 18 mins

Next.js Caching Layers

  1. Request Memoization — deduplicates identical fetch() calls in one render tree
  2. Data Cache — persists fetch results across requests and deployments
  3. Full Route Cache — caches rendered HTML & RSC payloads at build time
  4. Router Cache — client-side cache of visited route segments

Revalidation Options

  • Time-based: next: { revalidate: 60 }
  • On-demand: revalidatePath() and revalidateTag() in Server Actions

Caching & Revalidation Strategies

Caching is what separates a fast app from a slow one. Done right, your pages load instantly because the server already has the answer ready. Done wrong, users wait for the same data to be fetched over and over, or worse — they see stale data that never updates.

Next.js has one of the most sophisticated caching systems of any web framework — four distinct layers that each serve a different purpose. This lesson breaks all four down, shows you exactly when each one activates, and teaches you how to invalidate cached data on demand so your app always shows the right content at the right time.


1. The Four Caching Layers

Next.js caches at four different levels. Understanding each one individually prevents a huge amount of confusion about why data is or isn't updating:

Layer What it caches Where it lives Duration
Request Memoization Identical fetch() results within one render Server memory Single request lifetime
Data Cache fetch() responses Server filesystem / persistent store Until revalidated or redeployed
Full Route Cache Rendered HTML + RSC payload Server filesystem Until revalidated or redeployed
Router Cache RSC payloads of visited routes Browser memory Session (30s for dynamic, 5min for static)

Let's go through each one in depth so you know exactly what's happening and when to opt out.


2. Layer 1 — Request Memoization

You already saw this in Lesson 202. During a single server render, Next.js deduplicates identical fetch() calls — if your layout and your page both fetch the same URL, only one actual network request is made.

// Both of these run during the same render:
// app/layout.tsx
const user = await fetch("/api/me"); // ← Network request made here

// app/dashboard/page.tsx
const user = await fetch("/api/me"); // ← Returns memoized result, no network call

Key things to know about request memoization:

  • It only lasts for the duration of one server request — it's not persistent.
  • It only deduplicates GET requests — POST, PUT, DELETE are never memoized.
  • It only works for the native fetch() function. For database queries, use React's cache() wrapper.
  • You can't opt out of it — it's always on and has no downside.

3. Layer 2 — Data Cache

The Data Cache is Next.js's persistent server-side cache for fetch() responses. Unlike memoization which resets per request, the Data Cache persists across requests and even across deployments (on Vercel).

This is the cache you control with the cache and next options on fetch():

Opting Out — no-store

// Never cache — always go to the origin
const res = await fetch("https://api.example.com/live-scores", {
  cache: "no-store",
});

Use no-store for data that must always be fresh: live sports scores, real-time prices, anything where a cached response would be wrong.

Caching Forever — force-cache

// Cache indefinitely — never re-fetch until redeployed or manually revalidated
const res = await fetch("https://api.example.com/countries", {
  cache: "force-cache", // This is the default for fetch() in Next.js
});

Use force-cache for truly static data — a list of countries, supported currencies, or configuration from a CMS you control.

Time-based Revalidation

// Cache for 1 hour, then fetch fresh on the next request after expiry
const res = await fetch("https://api.example.com/products", {
  next: { revalidate: 3600 },
});

The revalidate number is in seconds. After the window expires, the next incoming request triggers a background re-fetch. The stale data is served to that first request while the fresh data is being fetched — this is called stale-while-revalidate. Subsequent requests get the fresh data.

Tag-based Revalidation

// Tag this fetch so it can be invalidated on demand
const res = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

Tags let you attach a label to a cached response. You can then call revalidateTag("posts") from a Server Action or Route Handler to instantly invalidate all cached responses with that tag — regardless of when they were cached. We'll see this in action in the revalidation section below.


4. Layer 3 — Full Route Cache

The Full Route Cache caches the rendered output of entire routes — the HTML and the RSC (React Server Component) payload — at build time or after the first request.

When Next.js builds your app (npm run build), it pre-renders every route it can — any route that doesn't use dynamic functions or no-store fetches gets rendered to static HTML and stored. When a user requests that route, the server serves the pre-rendered HTML instantly — no rendering, no data fetching, just serving a file. This is as fast as web serving gets.

Static vs Dynamic Routes

A route is statically rendered (Full Route Cache active) when it:

  • Doesn't use cookies(), headers(), or searchParams
  • Doesn't use fetch with cache: "no-store"
  • Doesn't use dynamic segment params that aren't pre-generated with generateStaticParams

A route is dynamically rendered (Full Route Cache bypassed) when it:

  • Uses cookies() or headers() — because these are per-request
  • Uses searchParams — because the URL can be anything
  • Uses fetch with cache: "no-store"

generateStaticParams — Pre-rendering Dynamic Routes

For dynamic routes like /blog/[slug], you can tell Next.js which slugs to pre-render at build time using generateStaticParams:

// app/blog/[slug]/page.tsx

// Tell Next.js which slugs to pre-render at build time
export async function generateStaticParams() {
  const posts = await db.post.findMany({ select: { slug: true } });

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.post.findUnique({ where: { slug } });

  return <article>{post?.title}</article>;
}

At build time, Next.js calls generateStaticParams, gets all the slugs, and pre-renders a static HTML file for each one. Requests to those blog posts are served from the Full Route Cache instantly — no server-side rendering needed at request time.


5. Layer 4 — Router Cache

The Router Cache is a client-side cache that lives in the browser. When a user navigates to a route, Next.js stores the RSC payload in memory. If they navigate away and come back within the cache window, the page is served from this in-memory cache — instantly, with no server request at all.

Cache durations:

  • Static routes: cached for 5 minutes
  • Dynamic routes: cached for 30 seconds

This is why clicking the back button in Next.js feels instant — you're getting the cached RSC payload, not re-rendering on the server.

Opting Out of the Router Cache

Sometimes you want the page to always re-fetch from the server — for example, after a user submits a form that changes data. Call router.refresh() to invalidate the Router Cache for the current route:

"use client";
import { useRouter } from "next/navigation";

export default function DeleteButton({ postId }: { postId: string }) {
  const router = useRouter();

  async function handleDelete() {
    await fetch(`/api/posts/${postId}`, { method: "DELETE" });
    router.refresh(); // Clears Router Cache and re-fetches server data
  }

  return <button onClick={handleDelete}>Delete Post</button>;
}

6. On-Demand Revalidation

Time-based revalidation is convenient but imprecise — data might be stale for up to the entire revalidation window. On-demand revalidation lets you invalidate the cache exactly when data changes — perfect for CMS content, e-commerce inventory, or any data you control.

There are two on-demand revalidation functions, both imported from next/cache:

revalidatePath — Invalidate a Specific Route

import { revalidatePath } from "next/cache";

// Call this after data changes to clear the cache for a specific path
revalidatePath("/blog");           // Clears cache for /blog
revalidatePath("/blog/my-post");   // Clears cache for one specific post
revalidatePath("/blog/[slug]", "page"); // Clears ALL blog post pages

revalidateTag — Invalidate by Tag

import { revalidateTag } from "next/cache";

// Clears all cached fetch() responses tagged with "posts"
revalidateTag("posts");

Real-world Example — Revalidating After a Server Action

The most common use case: a user creates or updates content, and you want the public page to immediately reflect that change:

// app/actions.ts
"use server";

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

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  const body = formData.get("body") as string;

  // 1. Save to database
  const post = await db.post.create({
    data: { title, body, published: true },
  });

  // 2. Invalidate the cache so the new post appears immediately
  revalidatePath("/blog");           // Blog listing page
  revalidatePath(`/blog/${post.slug}`); // The new post's page
  revalidateTag("posts");            // Any fetch tagged with "posts"

  // 3. Return the new post (optional)
  return post;
}
// app/blog/page.tsx
// This fetch is tagged — revalidateTag("posts") will clear it
const res = await fetch("https://api.example.com/posts", {
  next: { tags: ["posts"] },
});

The moment createPost runs, the cache is cleared. The next request to /blog re-renders with fresh data from the database. No waiting for a time window to expire.


7. Route Segment Config — Controlling Caching at the Page Level

Instead of setting cache options on individual fetch() calls, you can control caching for an entire route segment by exporting config variables from your page.tsx or layout.tsx:

// app/dashboard/page.tsx

// Force this entire route to be dynamic (never cached)
export const dynamic = "force-dynamic";

// OR — revalidate every 60 seconds
export const revalidate = 60;

// OR — force static (cache forever)
export const dynamic = "force-static";

These are the available options:

Export Value Effect
dynamic "auto" Default — Next.js decides based on what the route uses
dynamic "force-dynamic" Always render dynamically — never cache
dynamic "force-static" Force static rendering — cache forever
revalidate number Revalidate every N seconds
revalidate 0 Never cache — equivalent to force-dynamic
revalidate false Cache forever — equivalent to force-static

Route segment config is a blunt instrument — it applies to everything in the route. Prefer per-fetch cache options when you need granular control. Use route segment config when you want to make a blanket statement about an entire page (e.g. "this dashboard is always dynamic").


8. Choosing the Right Strategy — A Decision Guide

Here's how to pick the right caching strategy for any piece of data:

Data type Example Strategy
Never changes List of countries, currency codes force-cache (cache forever)
Changes rarely Blog posts, product descriptions revalidate: 3600 + revalidateTag on update
Changes frequently Comments, notifications, cart revalidate: 60 or no-store
Real-time Live scores, stock prices, chat no-store or WebSockets
Per-user Dashboard, profile, orders no-store + force-dynamic
User triggers update Create/edit/delete actions revalidatePath or revalidateTag in Server Action

9. Common Gotchas

  • Using cookies() or headers() makes the entire route dynamic. The moment you call either of these functions in a page or layout, Next.js opts the entire route out of the Full Route Cache. This is intentional — those values are per-request by nature — but it can surprise you if you call them in a layout that you expected to be cached.
  • Data Cache vs Full Route Cache are separate. You can have a route that's dynamically rendered (no Full Route Cache) but still benefits from the Data Cache for its fetch calls. Dynamic rendering doesn't mean no caching at all — it just means the HTML is generated fresh per request, while fetch responses may still be cached.
  • Revalidation in development is different. In npm run dev, the Data Cache and Full Route Cache are effectively disabled — every request fetches fresh data. This is intentional for easier debugging. Always test caching behaviour with a production build (npm run build && npm start).
  • revalidatePath clears the Full Route Cache, not the Data Cache. If you want to clear the underlying fetch data too, use revalidateTag alongside revalidatePath, or ensure your fetch uses no-store.
  • The Router Cache cannot be fully disabled. You can call router.refresh() to clear it for the current route, but it will re-populate on the next navigation. Accept it as a feature — it's what makes the back button feel instant.

Key Takeaways

  • Next.js has four caching layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache — each serving a different purpose.
  • Request Memoization deduplicates identical fetches within one render — automatic and always on.
  • The Data Cache persists fetch responses across requests — controlled via cache and next options on fetch().
  • The Full Route Cache stores pre-rendered HTML — routes are static by default unless they use dynamic functions or no-store.
  • The Router Cache lives in the browser and makes navigation feel instant — cleared with router.refresh().
  • Use revalidatePath() and revalidateTag() in Server Actions to invalidate the cache the moment data changes.
  • Caching is disabled in development — test with a production build to see real behaviour.

Next up: Lesson 204 — Streaming with Suspense. You'll learn how Next.js sends HTML to the browser in chunks, how to architect pages for maximum streaming performance, and how to handle the edge cases that trip people up.

Chat with us