Caching
Understand the four caching layers in Next.js -- request memoization, data cache, full route cache, and router cache.
Recipe
Quick-reference recipe card -- copy-paste ready.
// Layer 1: Request Memoization (automatic for fetch, manual for others)
// Same fetch URL is deduplicated within a single render pass
const data = await fetch("https://api.example.com/user/1"); // called in 3 components, only 1 request
// Manual memoization for non-fetch calls
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Layer 2: Data Cache (persists across requests)
await fetch(url, { next: { revalidate: 3600 } }); // cached 1 hour
await fetch(url, { cache: "no-store" }); // skip data cache
await fetch(url, { next: { tags: ["users"] } }); // tag for invalidation
// Layer 3: Full Route Cache (static pages at build time)
// Automatic for routes with no dynamic data
// Layer 4: Router Cache (client-side, in-memory)
// Automatic for <Link> prefetching and back/forward navigationWhen to reach for this: You need to understand why your data is stale, why a page is not updating, or how to optimize performance by leveraging the right cache layer.
Working Example
// lib/data.ts -- demonstrating all caching layers
import { cache } from "react";
import { db } from "@/lib/db";
// ---- Layer 1: Request Memoization ----
// This function can be called from multiple Server Components
// in the same render pass -- only one DB query runs
export const getCurrentUser = cache(async () => {
const session = await getSession();
if (!session) return null;
return db.user.findUnique({ where: { id: session.userId } });
});
// ---- Layer 2: Data Cache ----
export async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: { revalidate: 600, tags: ["products"] },
});
return res.json() as Promise<Product[]>;
}
// No caching -- always fresh
export async function getCartItems(userId: string) {
const res = await fetch(`https://api.example.com/cart/${userId}`, {
cache: "no-store",
});
return res.json() as Promise<CartItem[]>;
}// app/layout.tsx -- getCurrentUser called here...
import { getCurrentUser } from "@/lib/data";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getCurrentUser(); // Request 1 (actual DB call)
return (
<html lang="en">
<body>
<nav>
<span>{user?.name ?? "Guest"}</span>
</nav>
{children}
</body>
</html>
);
}// app/dashboard/page.tsx -- ...and also called here (deduplicated)
import { getCurrentUser } from "@/lib/data";
export default async function DashboardPage() {
const user = await getCurrentUser(); // Request 2 (memoized -- no DB call)
return <h1>Welcome back, {user?.name}</h1>;
}What this demonstrates:
React.cachededuplicating a database call across two Server Components in the same rendernext.revalidateplacing fetch results into the Data Cachecache: "no-store"bypassing the Data Cache for user-specific data- The layout and page both calling
getCurrentUser()without triggering two queries
Deep Dive
How It Works
- Request Memoization runs during a single server render. When
fetchis called with the same URL and options, React returns the memoized result. For non-fetch calls (database, ORM), wrap the function withReact.cache(). Memoization is cleared after the render completes. - Data Cache persists fetched data across requests and deployments on the server. It is keyed by the fetch URL and options. It respects
next.revalidatefor time-based expiry andnext.tagsfor on-demand invalidation viarevalidateTag(). - Full Route Cache stores the complete HTML and RSC payload for statically rendered routes at build time. When a user requests a static page, the cached version is served instantly. Dynamic routes (those using
cookies(),headers(),searchParams, orcache: "no-store") are excluded. - Router Cache is a client-side, in-memory cache that stores prefetched route segments. When the user navigates with
<Link>, the next page may already be cached. The router cache has different lifetimes: 30 seconds for dynamic pages, 5 minutes for static pages (these are defaults and configurable in Next.js 15+).
Variations
Opting out of all caching for a route:
// app/real-time/page.tsx
export const dynamic = "force-dynamic"; // skips Full Route Cache
export const fetchCache = "force-no-store"; // skips Data Cache for all fetchesUsing unstable_cache for non-fetch data:
import { unstable_cache } from "next/cache";
const getCachedPosts = unstable_cache(
async () => db.post.findMany(),
["all-posts"], // cache key parts
{
tags: ["posts"],
revalidate: 3600,
}
);Controlling Router Cache behavior:
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
staleTimes: {
dynamic: 0, // don't cache dynamic pages on the client
static: 300, // cache static pages for 5 minutes
},
},
};
export default config;TypeScript Notes
// React.cache preserves the function signature
import { cache } from "react";
const getUser = cache(
async (id: string): Promise<User | null> => {
return db.user.findUnique({ where: { id } });
}
);
// getUser: (id: string) => Promise<User | null>
// Segment config types
export const dynamic: "auto" | "force-dynamic" | "force-static" | "error" = "auto";
export const revalidate: number | false = 60;
export const fetchCache: "auto" | "default-cache" | "only-cache" |
"force-cache" | "force-no-store" | "default-no-store" | "only-no-store" = "auto";Gotchas
-
Confusing request memoization with the Data Cache -- Request memoization is per-render and ephemeral; the Data Cache persists across requests. Fix: Use
React.cache()for per-request deduplication andnext.revalidate/next.tagsfor persistent caching. -
Router Cache serving stale pages -- After a mutation, the client-side Router Cache may still serve the old page during back/forward navigation. Fix: Call
router.refresh()in the Client Component after a Server Action, or configurestaleTimesto reduce client-side cache duration. -
cache: "force-cache"is not always the default -- In Next.js 15+, the default iscache: "auto", which may vary depending on context (dynamic functions, route config). Fix: Be explicit about caching behavior by settingcacheornext.revalidateon every fetch. -
unstable_cachekey collisions -- If twounstable_cachecalls share the same key parts but return different data shapes, you get corrupted cache. Fix: Use unique, descriptive key arrays. -
Middleware and Edge Runtime do not share caches -- The Data Cache is per-deployment-region. Middleware running at the edge may not see the same cache as a Node.js server function. Fix: Be aware of cache boundaries in multi-region deployments.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
React.cache() | You need per-request deduplication for non-fetch calls | You need data to persist across requests |
unstable_cache | You need Data Cache semantics for database or ORM calls | Plain fetch with next.revalidate works |
| Redis or Memcached | You need a shared cache across multiple server instances | The built-in Next.js cache is sufficient |
SWR stale-while-revalidate | You want client-side caching with automatic revalidation | Server-side caching covers your needs |
cache: "no-store" everywhere | Data must always be fresh (real-time dashboards) | You can tolerate stale data for better performance |
FAQs
What are the four caching layers in Next.js and how do they differ?
- Request Memoization: per-render, deduplicates identical fetch calls within a single server render pass
- Data Cache: persists across requests and deployments, keyed by fetch URL and options
- Full Route Cache: stores prerendered HTML and RSC payload for static routes at build time
- Router Cache: client-side in-memory cache for prefetched route segments during navigation
How do you deduplicate database queries that bypass automatic fetch memoization?
import { cache } from "react";
import { db } from "@/lib/db";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});- Wrap the function with
React.cache()for per-request deduplication
What is the default cache behavior for fetch in Next.js 15+?
- The default is
cache: "auto", notforce-cacheas in Next.js 14 - The framework decides caching behavior based on context (dynamic functions, route config)
- Be explicit by setting
cacheornext.revalidateon every fetch
How long does the Router Cache keep pages on the client side?
- 30 seconds for dynamic pages
- 5 minutes for static pages
- These are defaults in Next.js 15+ and can be configured via
staleTimesinnext.config.ts
How do you opt a route out of all caching layers?
export const dynamic = "force-dynamic";
export const fetchCache = "force-no-store";force-dynamicskips the Full Route Cacheforce-no-storeskips the Data Cache for all fetches in the route
What causes the Router Cache to serve stale data after a mutation?
- The client-side Router Cache does not automatically invalidate after Server Actions
- Back/forward navigation may show the old page
- Call
router.refresh()in the Client Component or reducestaleTimesin config
What happens if two unstable_cache calls share the same key parts but return different data?
- You get corrupted cache -- the wrong data shape is returned
- Always use unique, descriptive key arrays for each
unstable_cachecall - This is a common source of hard-to-debug production bugs
How does React.cache() preserve TypeScript types?
import { cache } from "react";
const getUser = cache(
async (id: string): Promise<User | null> => {
return db.user.findUnique({ where: { id } });
}
);
// getUser: (id: string) => Promise<User | null>- The wrapped function retains the exact same signature and return type
Does Middleware share the same Data Cache as Node.js server functions?
- No. The Data Cache is per-deployment-region
- Middleware running at the edge may not see the same cache as a Node.js server function
- Be aware of cache boundaries in multi-region deployments
When should you use unstable_cache versus React.cache()?
React.cache()provides per-request deduplication only; data does not persist across requestsunstable_cacheprovides Data Cache semantics (persists across requests, supports tags and revalidation)- Use
unstable_cachefor database or ORM calls that need cross-request caching
How do you configure the Router Cache lifetime for dynamic pages?
// next.config.ts
const config: NextConfig = {
experimental: {
staleTimes: {
dynamic: 0, // don't cache dynamic pages on the client
static: 300, // cache static pages for 5 minutes
},
},
};What is the type for the fetchCache segment config export?
export const fetchCache:
| "auto"
| "default-cache"
| "only-cache"
| "force-cache"
| "force-no-store"
| "default-no-store"
| "only-no-store" = "auto";Related
- Fetching -- Setting cache options on fetch calls
- Revalidation -- Invalidating cached data
- Static vs Dynamic -- How caching determines rendering mode
- Streaming -- Streaming cached and dynamic content