Skip to main content
{/}codefunc
GuidesJuly 7, 2026 · 8 min read

Next.js Caching and Data Fetching: The Four Cache Layers Explained

A practical guide to caching in Next.js 15+ — Request Memoization, Data Cache, Full Route Cache, and Router Cache demystified with patterns for static generation, ISR, and on-demand revalidation.

By codefunc

nextjscachingreactssrisrdata-fetchingperformance

Caching is the most confusing part of Next.js. The framework has four distinct cache layers that interact in non-obvious ways. Defaults changed between versions. Documentation mixes legacy APIs with modern ones. If you have ever deployed a page that stubbornly shows stale data, or added cache: 'no-store' everywhere out of frustration, this guide will clarify how caching actually works.

Bottom line: Next.js 15+ has four cache layers: Request Memoization (per-render), Data Cache (persistent fetch), Full Route Cache (pre-rendered HTML), and Router Cache (client navigation). Since Next.js 15, fetch is NOT cached by default — you must opt in. Use revalidate for time-based freshness, revalidateTag for on-demand invalidation after mutations.

The four cache layers

Layer Scope Duration Purpose
Request Memoization Single render Request lifetime Dedupe identical fetches
Data Cache Server Persistent Cache fetch results
Full Route Cache Server Persistent Cache rendered HTML/RSC
Router Cache Client Session Cache visited routes

Understanding when each layer is used — and when it is bypassed — is essential for predictable behavior.

Request Memoization

When you call the same fetch in multiple components during a single render, Next.js deduplicates them automatically.

// layout.tsx
async function Layout({ children }) {
  const user = await fetchUser(); // fetch #1
  return <div>{children}</div>;
}

// page.tsx
async function Page() {
  const user = await fetchUser(); // same URL — memoized, not re-fetched
  return <Profile user={user} />;
}

How it works

  • Matches by URL and options
  • Lasts for single render pass
  • Happens automatically for fetch
  • Does NOT persist between requests

When memoization helps

  • Shared data needed in layout and page
  • Multiple components fetching same resource
  • Avoiding prop drilling by fetching where needed

Data Cache

The Data Cache stores fetch results persistently on the server. This is where ISR (Incremental Static Regeneration) lives.

Next.js 15 default: uncached

// NOT cached by default in Next.js 15+
const data = await fetch('https://api.example.com/posts');

Opting into caching

// Cache indefinitely (until redeployed)
const data = await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
});

// Cache with time-based revalidation (ISR)
const data = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 }, // revalidate after 1 hour
});

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

Revalidation strategies

Strategy When to use
revalidate: 60 Content changes periodically (blog posts)
revalidate: 0 Always fresh (user-specific data)
cache: 'force-cache' Static content that rarely changes
cache: 'no-store' Real-time data, authenticated requests

Tags for granular invalidation

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

// Invalidate by tag (in Server Action)
import { revalidateTag } from 'next/cache';

async function createPost(data) {
  await db.posts.create(data);
  revalidateTag('posts'); // bust the cache
}

Full Route Cache

Next.js pre-renders routes at build time (static) or on first request (dynamic). The rendered HTML and React Server Component payload are cached.

Static routes (default when possible)

// Fully static — cached at build time
export default function AboutPage() {
  return <div>About us</div>;
}

Dynamic routes

Routes become dynamic when they use:

  • cookies(), headers(), searchParams
  • fetch with cache: 'no-store'
  • export const dynamic = 'force-dynamic'
// Dynamic — rendered on every request
import { cookies } from 'next/headers';

export default async function DashboardPage() {
  const session = cookies().get('session');
  const user = await fetchUser(session);
  return <Dashboard user={user} />;
}

Force behaviors

// Force static generation
export const dynamic = 'force-static';

// Force dynamic rendering
export const dynamic = 'force-dynamic';

// Set revalidation for entire route
export const revalidate = 3600; // 1 hour

Router Cache (Client-side)

The Router Cache stores visited routes in the browser during a session. This enables instant back/forward navigation.

Behavior

  • Prefetched routes are cached for 30 seconds (dynamic) or 5 minutes (static)
  • Visited routes are cached for the session duration
  • Hard navigation (window.location) bypasses the cache

Invalidating Router Cache

import { useRouter } from 'next/navigation';

function Component() {
  const router = useRouter();
  
  async function handleUpdate() {
    await updateData();
    router.refresh(); // Invalidate Router Cache for current route
  }
}

Common caching patterns

Pattern 1: Static marketing pages

// app/page.tsx
export const revalidate = 86400; // Revalidate daily

export default async function HomePage() {
  const content = await fetch('https://cms.example.com/home', {
    next: { tags: ['home'] },
  });
  return <HomeContent data={content} />;
}

Pattern 2: Blog with ISR

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // Revalidate hourly

export async function generateStaticParams() {
  const posts = await fetchAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
    next: { tags: [`post-${params.slug}`] },
  });
  return <Article post={post} />;
}

Pattern 3: Dashboard with fresh data

// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';

export default async function DashboardPage() {
  const stats = await fetch('https://api.example.com/stats', {
    cache: 'no-store',
  });
  return <Dashboard stats={stats} />;
}

Pattern 4: CMS with on-demand revalidation

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

export async function POST(request: Request) {
  const { tag, secret } = await request.json();
  
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 });
  }
  
  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

Webhook from CMS calls this endpoint when content changes.

Server Components vs Client Components

Server Components (default)

// Runs on server, can fetch directly
export default async function ProductList() {
  const products = await fetch('https://api.example.com/products');
  return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
}

Client Components

'use client';

// Runs in browser, needs useEffect or library
import { useState, useEffect } from 'react';

export default function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(setResults);
  }, [query]);
  
  return <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>;
}

When to use each

Use Server Components for Use Client Components for
Data fetching Interactive UI (forms, buttons)
Accessing backend resources Browser APIs (localStorage)
Keeping secrets server-side Event handlers (onClick)
Large dependencies useState, useEffect
SEO-critical content Real-time updates

The use cache directive (Next.js 15+)

Beyond fetch, you can cache any async function:

async function getUser(id: string) {
  'use cache';
  // This result is cached
  return await db.users.findUnique({ where: { id } });
}

With tags and lifetime

import { cacheTag, cacheLife } from 'next/cache';

async function getPosts() {
  'use cache';
  cacheTag('posts');
  cacheLife('hours'); // Preset: stale 5m, revalidate 1h, expire 1d
  
  return await db.posts.findMany();
}

Cache life presets

Profile Stale Revalidate Expire
seconds 0 1s 60s
minutes 5m 1m 1h
hours 5m 1h 1d
days 5m 1d 1w
weeks 5m 1w 30d
max 5m 30d ~indefinite

Debugging caching issues

Check what's cached

// Add to fetch to see caching behavior
const data = await fetch(url, {
  next: { revalidate: 60, tags: ['debug'] },
});
console.log('Cache status:', response.headers.get('x-nextjs-cache'));

Common issues

Symptom Likely cause Fix
Stale data after deploy Router Cache router.refresh() or hard reload
Data never updates cache: 'force-cache' without revalidation Add revalidate or tags
Too many API calls Missing memoization Ensure same URL/options
Dynamic when should be static Using cookies()/headers() Move to client or pass as props
Build fails with dynamic generateStaticParams missing Add or use dynamicParams

Validate JSON payloads

When debugging API responses, format them with the JSON Formatter to inspect structure. Validate syntax with the JSON Validator before blaming the cache.

Route Segment Config

Configure caching at the route level:

// app/dashboard/layout.tsx

// Force all child routes to be dynamic
export const dynamic = 'force-dynamic';

// Or set revalidation for all child routes
export const revalidate = 60;

// Opt into Partial Prerendering
export const experimental_ppr = true;

Options

Option Values Effect
dynamic 'auto', 'force-dynamic', 'force-static', 'error' Control rendering mode
revalidate false, 0, number Time-based revalidation
fetchCache 'auto', 'force-cache', 'force-no-store', etc. Default fetch behavior
runtime 'nodejs', 'edge' Execution environment

Migration from Pages Router

Pages Router App Router
getStaticProps Server Component + fetch with cache
getServerSideProps Server Component + dynamic = 'force-dynamic'
getStaticPaths generateStaticParams
revalidate in getStaticProps revalidate route config or fetch option
unstable_revalidate revalidatePath / revalidateTag

Practical takeaway

Next.js caching is powerful but has a learning curve. Start with the defaults — uncached fetch in Next.js 15+. Add caching explicitly where it helps: revalidate for time-based freshness, tags for on-demand invalidation. Use Server Components for data fetching, Client Components for interactivity. When data is stale, check all four cache layers. When debugging, log cache headers and use router.refresh() to clear client cache.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool