Posted on: 05/07/2026(updated)
The Next.js App Router features four distinct caching layers. They interact closely with one another, yet each serves a completely different purpose in the lifecycle of a request.
Here's a breakdown of how each layer works, how it triggers, and how to use it in practice.
Request Memoization is a feature that deduplicates identical data fetches within a single server-side render pass. If you need the exact same data in multiple components across your page component tree, you can fetch it multiple times without hitting the network repeatedly.
Next.js extends the native JavaScript fetch API. When a fetch request is made on the server, Next.js checks an in-memory cache using a key generated from the request URL and options. If the key matches an ongoing or completed request within that specific lifecycle, it reuses the result. Because this cache lives in server memory and is tied strictly to the current request lifecycle, it is automatically wiped out as soon as the server finishes rendering the page.
Imagine an e-commerce page where both the page header and the main product card need the current user's profile information. Instead of fetching the user profile in a parent component and drilling props down through dozens of layers, you simply call fetch in both places.
// app/layout.tsx
async function Navbar() {
// First execution: Triggers a real network request
const user = await fetch(
'https://api.example.com/user/me'
);
return (
<nav>Welcome back, {user.name}</nav>
);
}
// app/products/[id]/page.tsx
async function ProductPage() {
/**
* Second execution:
* Instantly resolved from server memory
*/
const user = await fetch(
'https://api.example.com/user/me'
);
return <div>Recommended for {user.name}</div>;
}
So instead of treating fetch as a heavy, costly operation that you must carefully coordinate, Next.js and React encourage you to treat it as a data provider.
If Component A and Component B both need the user's profile data, you don't drill props down through five layers of components, and you don't wrap them in a global React Context provider. You simply fetch the data directly inside both Component A and Component B.
While this pattern makes your code clean and decoupled, there are 3 important rules to remember:
GET requests. Next.js only memoizes GET requests. POST, PUT, and DELETE requests are not automatically deduplicated.The Data Cache is a persistent, server-side cache designed to store API response data across multiple user requests, multiple sessions, and even server restarts. It ensures your backend database or third-party APIs aren't overwhelmed by repetitive traffic.
Unlike Memoization (which stores data temporarily in memory), the Data Cache persists data to a filesystem, data store, or cloud blob storage depending on your deployment environment. When a fetch request is made, Next.js checks this persistent store. If a valid cached response exists, it is returned immediately. If the cache has expired or doesn't exist, Next.js performs the network request, stores the new data in the cache, and returns it.
Suppose you are pulling blog posts from a Headless CMS. The data changes infrequently, so you want to cache it globally but check for updates every hour.
// services/blog.ts
/**
* Scenario A:
* Cached indefinitely until a manual webhook
* invalidation
*/
export async function getGlobalSettings() {
return fetch('https://api.cms.com/settings');
}
/**
* Scenario B:
* Cached with Time-Based Revalidation (TTL of 1 hour)
*/
export async function getBlogPosts() {
return fetch('https://api.cms.com/posts', {
next: { revalidate: 3600 } // 3600 seconds = 1 hour
});
}
/**
* Scenario C:
* Opting out entirely (Dynamic data)
*/
export async function getLiveStockPrices() {
return fetch('https://api.example.com/stocks', {
cache: 'no-store'
});
}
TTL (Time-To-Live) and opting out of caching (dynamic fetching) have existed for decades --long before Next.js, React or any other modern JavaScript full-stack frameworks were born.
Next.js did not invent these concepts, but rather abstract them into the application layer, and tie them directly to React components.
Backend frameworks like Ruby on Rails, Django, or Express.js used standard HTTP headers to tell browsers and Reverse Proxies (like Nginx, Cloudfare) how to handle data:
Next.js is a full-stack framework but leans heavily on the client/React model. What Next.js has done is moved these backend configurations directly into the data-fetching layer inside UI components.
The Full Route Cache is a server-side mechanism that stores the completely rendered HTML and React Server Component (RSC) payload for entire routes. Instead of rendering a page line-by-line on every single visitor request, Next.js serves the pre-compiled output instantly.
During next build, Next.js analyzes your application's routes. If a route does not utilize dynamic functions (like reading cookies or headers) and its data fetches are cacheable, Next.js compiles the route into static HTML files and RSC binary data. When a user requests that URL, the server skips the React rendering cycle completely and returns the cached files directly from the disk.
A company's "About Us" or "Terms of Service" page doesn't change based on who is logged in. This makes them perfect candidates for the Full Route Cache. Conversely, an account dashboard requires a dynamic opt-out.
// app/about/page.tsx
/** ✅ Statically generated at build time.
* Completely stored in the Full Route Cache.
*/
export default function AboutPage() {
return (
<main>
<h1>About Our Company</h1>
<p>Established in 2020...</p>
</main>
);
}
// app/dashboard/page.tsx
/** ❌ Opt-out:
* Forces the server to compute the page
* fresh on every request
*/
export const dynamic = 'force-dynamic';
export default function DashboardPage() {
return (
<div>
Live System Status: {
new Date().toLocaleTimeString()
}
</div>
);
}
The Router Cache is a client-side, browser-based cache. It stores the layout and page structures of previously visited routes (and pre-fetched links) in the user's browser memory for the duration of their session.
As a user navigates around your application or encounters <Link> components, Next.js pre-fetches and stores those page segments in the browser's temporary memory. When a user clicks a link or uses the browser's back/forward buttons, the transition happens instantly without sending a new request back to the web server. This cache resets automatically when the user refreshes the page or closes the tab.
Consider a layout-heavy dashboard with side navigation. As you click between sub-tabs, the sidebar layout remains intact, and only the page contents swap out seamlessly.
// app/dashboard/layout.tsx
import Link from 'next/link';
export default function DashboardLayout({ children }:
{ children: React.ReactNode }) {
return (
<div className="flex">
{/*
Hovering over or viewing these links will
cause the browser to pre-fetch and store
the routes in the Router Cache
*/}
<aside className="sidebar">
<Link
href="/dashboard/analytics">Analytics
</Link>
<Link
href="/dashboard/settings">Settings
</Link>
</aside>
{/*
Clicking links swaps this content instantly
without full page reloads
*/}
<main className="content">{children}</main>
</div>
);
}
If your data is stale, figure out which layer is holding onto it using this matrix:
| Question | If YES | If NO |
|---|---|---|
| Is the stale data appearing immediately on the first page load? | Data Cache or Full Route Cache | Router Cache (Client-side) |
| Does a hard refresh (Cmd+Shift+R) fix the issue? | Router Cache | Look at the Server-side caches |
| Does redeploying the application completely fix it? | Full Route Cache | Data Cache misconfiguration |
By default, Next.js caches all raw fetch requests. If you want real-time data, you must explicitly pass options to opt out.
// ❌ Will be heavily cached by Next.js automatically
const res = await fetch(
'https://api.example.com/profile'
);
// ✅ Correct: Bypasses the Data Cache entirely
const res = await fetch(
'https://api.example.com/profile', {
cache: 'no-store'
});
You can also dictate caching defaults for entire files using route segment configs at the top of your page.tsx or layout.tsx files.
/* Force the route to render
* dynamically on every request
*/
export const dynamic = 'force-dynamic';
/** Cache the entire route, re-evaluating it
* every 60 seconds max
*/
export const revalidate = 60;
When you call an unpredictable server-side utility like cookies() or headers(), Next.js assumes the page output cannot be predicted. Consequently, it automatically shifts the entire route out of static generation and turns off the Full Route Cache.
import { cookies } from 'next/headers';
export default async function SettingsPage() {
/** ⚠️ Calling this function immediately
* turns this entire page dynamic */
const cookieStore = await cookies();
const theme = cookieStore.get('theme');
/** This fetch is now forced to run dynamically
* on every single request too
*/
const profileData = await fetch(
'https://api.example.com/profile'
);
return (
<div className={theme?.value}>
Settings Panel
</div>
);
}
If you need dynamic elements (like themes, user sessions, or search parameters) alongside highly static content, isolate the dynamic logic inside a sub-component wrapped in a React <Suspense> boundary.
import { Suspense } from 'react';
import { cookies } from 'next/headers';
/** 1. The main page stays static and
* optimized in the Full Route Cache */
export default function LayoutPage() {
return (
<div>
<StaticSidebar /> {/* Heavily cached */}
<Suspense fallback={<p>Loading profile...</p>}>
<DynamicProfilePanel />
</Suspense>
</div>
);
}
// 2. The dynamic heavy-lifting is contained here
async function DynamicProfilePanel() {
const cookieStore = await cookies();
const userToken = cookieStore.get('session');
const profile = await fetch(
'https://api.example.com/profile', {
headers: { Authorization: `Bearer ${userToken}` }
});
return <div>{profile.name}</div>;
}
This happened to me while building this blog application. Let's say I signed into the application. Then, I write a post and signed out. However, if I try to click on create a post again, instead of I being redirected to the Signin page, it sends me back to the create a post form page.
Now, if you try submitting the create a post form, it will almost certainly fail on the backend, meaning your authentication itself works fine. Your authentication provider deleted your session cookie or invalidated your JWT token when you signed out.
It happens because of Next.js being static by default (or aggressively caching pages). Create a post page (or the page it redirected to) didn't use any explicitly dynamic data when Next.js compiled it. Next.js bundled it into the Full Route Cache at build time, and the server blindly served the cache HTML/RSC payload from when you were first authenticated, trapping you in a loop.
To ensure the page evaluates fresh on every single request (checking your true authentication state instead of serving an old snapshot), add the route segment config to the top of the file handling the redirect or the form:
// app/create-post/page.tsx or app/dashboard/page.tsx
/** 🚀 Force Next.js to completely bypass
* the Full Route Cache for this route
*/
export const dynamic = 'force-dynamic';
export default async function CreatePostPage() {
/** Your authentication check and
* form component here
*/
}