All insights
Engineering8 min read

Next.js Caching Explained: All Four Layers

Next.js has four distinct caching mechanisms. Most developers understand one or two of them. Understanding all four is what separates fast apps from truly fast apps.

NC

Nextcraft Engineering Team

Why Caching Is Confusing in Next.js

Next.js has four separate caching layers that interact with each other. The documentation covers each one, but rarely explains how they compose. This leads to developers either caching too aggressively (stale data) or not at all (slow apps).

Let's build a mental model from the bottom up.

Layer 1: Request Memoization

Scope: Single render cycle (one request)
Location: In-memory, per-request

When you call fetch() with the same URL multiple times in a single server render, Next.js deduplicates those calls. The first call hits the network; subsequent identical calls return the cached result from memory.

code
// Both of these hit the network only once per request
async function UserAvatar() {
  const user = await fetch('/api/me').then(r => r.json());
  return <img src={user.avatar} />;
}

async function UserName() {
  const user = await fetch('/api/me').then(r => r.json());
  return <span>{user.name}</span>;
}

This is automatic. You get it for free with fetch. For other data sources (databases, ORMs), use React's cache() function to opt in:

code
import { cache } from 'react';

export const getUser = cache(async (id: string) => {
  return db.users.findUnique({ where: { id } });
});

When it clears: After the request completes. Never persists.

Layer 2: Data Cache

Scope: Persistent across requests
Location: Server-side (Vercel's data store or Node.js filesystem)

This is the fetch cache. By default, fetch in Next.js Server Components caches the response indefinitely. You control it with the cache and next.revalidate options:

code
// Cache forever (default)
const data = await fetch('https://api.example.com/products');

// Don't cache at all
const data = await fetch('https://api.example.com/cart', {
  cache: 'no-store',
});

// Cache and revalidate every 60 seconds
const data = await fetch('https://api.example.com/prices', {
  next: { revalidate: 60 },
});

// Cache with tag-based invalidation
const data = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] },
});

Tag-based invalidation lets you purge specific cached data on demand — critical for CMS integrations and webhook-driven revalidation:

code
// In a webhook handler
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  revalidateTag('posts'); // Invalidates all fetches tagged 'posts'
  return Response.json({ revalidated: true });
}

When it clears: On revalidation, manual revalidateTag()/revalidatePath(), or deployment.

Layer 3: Full Route Cache

Scope: Rendered HTML and RSC payload
Location: Server-side

If a route is statically renderable (no uncached dynamic data, no cookies/headers used), Next.js caches the entire rendered output — both the HTML and the React Server Component payload. Subsequent requests for that route are served from cache without re-running your components.

This is what makes static generation in the App Router so powerful. Pages that don't need per-request data get cached at build time and served in milliseconds.

code
// This page will be fully cached — no dynamic data
export default async function AboutPage() {
  const content = await fetch('https://cms.example.com/about', {
    next: { revalidate: 3600 },
  });
  return <AboutContent data={await content.json()} />;
}

You can opt routes out of full route caching:

code
export const dynamic = 'force-dynamic'; // Always re-render
export const revalidate = 0;            // Same effect

When it clears: On revalidation, or when the Data Cache for any fetch used on the route is invalidated.

Layer 4: Router Cache

Scope: Navigation history
Location: Client-side, in-memory

This is the only client-side cache. When a user navigates between pages, Next.js caches the RSC payload for each route they visit. Back/forward navigation is instant — no network request needed.

The Router Cache has time-based expiry:

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

You can't manually invalidate the Router Cache from the server. It clears on full page reload, or programmatically via router.refresh():

code
'use client';
import { useRouter } from 'next/navigation';

function RefreshButton() {
  const router = useRouter();
  return (
    <button onClick={() => router.refresh()}>
      Refresh
    </button>
  );
}

How the Layers Interact

A request flows through the caches in order:

  1. Router Cache — is this route cached client-side? Serve it. Done.
  2. Full Route Cache — is the rendered output cached server-side? Serve it. Done.
  3. Data Cache — for each fetch in the route, is the response cached? Use it.
  4. Request Memoization — deduplicate any identical fetches within this render.

Understanding this flow explains seemingly mysterious behavior:

  • You call revalidatePath() but the page still shows old data → the Router Cache on the client hasn't been cleared
  • Your data updates but the page doesn't → the Full Route Cache is serving stale HTML
  • Two components fetch the same URL but you're being charged double API calls → you need cache() on a non-fetch data source

A Practical Caching Strategy

For most Next.js applications:

code
// Static marketing pages — cache everything
export const revalidate = 3600; // Revalidate hourly

// Blog posts — cache with tag-based invalidation
const post = await fetch(`/api/posts/${slug}`, {
  next: { tags: [`post-${slug}`] },
});

// User-specific data — never cache
const session = await fetch('/api/session', {
  cache: 'no-store',
});

// Product prices — short TTL
const prices = await fetch('/api/prices', {
  next: { revalidate: 60 },
});

The goal: cache everything that can be cached, at the most granular level possible. Serve from edge where possible. Revalidate on data change, not on a fixed timer.

Stay Informed.

Join 1,200+ founders and engineers receiving our monthly deep dives on product engineering, design, and growth.

Insights once a month. No spam. Unsubscribe anytime.