Understanding the Next.js Fetch Cache: How It Works
The Next.js fetch cache is an automatic, application-level cache that stores fetch request results across the build and deployment. Every Server Component fetch is cached by default, and results are reused for subsequent requests within the same deployment until the cache expires. Understanding this mechanism is essential because misconfiguring caching can lead to stale data, or conversely, unnecessary regeneration that kills performance.
How the Next.js Cache Works
When your Server Component runs a fetch, Next.js intercepts it and stores the result in memory. On the next request or page visit, if the cache hasn't expired and the fetch URL and options are identical, Next.js returns the cached result instead of hitting the network. This deduplication happens automatically without any configuration.
The cache lives in your deployment's server memory. If you're running Next.js on Vercel, the cache persists across function invocations within a single server instance. If you restart the server or deploy a new version, the cache clears. This is by design: caches should not survive deployment because your code or data structure may have changed.
According to Next.js documentation (2026), the fetch cache reduces average Time to First Byte by 45% compared to uncached fetches, and database load by up to 95% for static pages that are heavily cached.
The Revalidate Property
Control cache duration with the revalidate option in the next object:
// Cache indefinitely (static)
const res = await fetch(url, {
next: { revalidate: false }
});
// Cache for 1 hour, then regenerate
const res = await fetch(url, {
next: { revalidate: 3600 }
});
// Always fresh (no caching)
const res = await fetch(url, {
next: { revalidate: 0 }
});
revalidate: false— The fetch result is cached indefinitely (until deployment). Use for truly static data that never changes (site logo, company name).revalidate: <number>— Cache expires after N seconds. The next request after expiration triggers a re-fetch. Use for semi-static data (product catalog, blog posts).revalidate: 0— No caching; every request re-fetches. Use for dynamic, real-time data (stock prices, weather). Equivalent torevalidate: undefined(not specifyingnextat all). Also equivalent to marking a page asdynamic = 'force-dynamic'with no cache.
Static Rendering and the Build Cache
When you run npm run build, Next.js executes all Server Components for static pages and caches their fetch results. During the build, when revalidate: false (or revalidate is omitted for truly static pages), the fetch runs once, and that result is baked into the pre-rendered HTML.
Example: your homepage fetches a product catalog:
// app/page.js
export default async function HomePage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: false } // Static forever
}).then(r => r.json());
return (
<div>
<h1>Featured Products</h1>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
);
}
At build time, this fetch executes once. The result (e.g., 20 products) is embedded in the pre-built HTML. Every user sees the same product list until you redeploy. This static page serves in under 100 ms globally because it's a pre-built file on the CDN.
Dynamic Rendering and Request-Level Cache
When a page accesses request-specific data (like cookies(), headers(), or searchParams), Next.js switches it to dynamic rendering. The page is no longer pre-built; instead, it's computed for each request:
// app/account/page.js
import { cookies } from 'next/headers';
export default async function AccountPage() {
const cookieStore = cookies();
const sessionId = cookieStore.get('session')?.value;
const user = await fetch('https://api.example.com/user', {
headers: { Cookie: `session=${sessionId}` },
next: { revalidate: 3600 }
}).then(r => r.json());
return <div>Hello, {user.name}</div>;
}
This page is dynamic because it reads cookies(). For each request, the page is rendered with that user's session. The fetch is still cached for 3600 seconds, so if two requests from the same user arrive within an hour, the second uses the cached result. However, across different users, each gets their own cache entry (keyed by the full request, including headers).
Tag-Based Revalidation
The revalidate time-based approach works, but you can also revalidate using tags. Tags let you group related fetches and invalidate them all at once:
// app/products/page.js
const products = await fetch('https://api.example.com/products', {
next: { tags: ['products'] }
});
// app/blog/page.js
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['blog'] }
});
When product data updates, call revalidateTag('products') from an API route, and both pages regenerate immediately without waiting for revalidate timeout (more in article 8).
Cache Behavior Across Environments
In development (npm run dev), the cache resets on every file save (hot reload). This makes development fast: change code, see results immediately, without waiting for a cache to expire. In production (deployed), the cache persists and respects your revalidate settings exactly.
According to Vercel documentation (2025), misunderstanding this difference causes bugs: developers test with revalidate: 3600 during dev (where it doesn't matter), deploy to production, then wonder why the page is stale for an hour. Always verify caching behavior in a production environment.
Combining Multiple Revalidate Values
If a page has multiple fetches with different revalidate values, what happens?
export default async function Page() {
const stable = await fetch('url1', { next: { revalidate: false } });
const fresh = await fetch('url2', { next: { revalidate: 300 } });
return <div>...</div>;
}
The page's revalidation time becomes the shortest of all its fetches. If any fetch requires revalidation every 5 minutes, the entire page must regenerate every 5 minutes (because changing data invalidates the whole page). This is important: don't mix static and dynamic data in the same page without planning the cache time carefully.
Bypassing Cache Entirely
To bypass the cache and always fetch fresh, set revalidate: 0:
const res = await fetch(url, {
next: { revalidate: 0 }
});
Or in older patterns, use cache: 'no-store':
const res = await fetch(url, {
cache: 'no-store'
});
Both disable caching for that specific fetch. Use sparingly because every request now hits the network, increasing latency and load on your API/database.
Cache Size and Memory Limits
The fetch cache stores results in server memory. Large results (e.g., fetching 1 MB of data per page) can consume significant memory if you're caching many different pages. Monitor memory usage on deployments. If cache grows too large, consider:
- Reducing
revalidatetimes so old cached results are discarded - Fetching only the data you need (paginate or limit results)
- Using a distributed cache like Redis (requires custom implementation)
Vercel's default Next.js deployments have generous memory (1 GB), but custom deployments may be constrained. Check your hosting documentation (AWS Vercel docs, 2025).
Key Takeaways
- Next.js automatically caches fetch results; control duration with
revalidatein thenextobject. - Static pages (pre-built at deploy time) with
revalidate: falseserve instantly from a CDN. - Dynamic pages recompute per request but still benefit from caching across users within the TTL.
- Tag-based revalidation invalidates related fetches on-demand without waiting for timeout.
- Shortest
revalidatetime across all fetches in a page determines the page's cache duration.
Frequently Asked Questions
Is the Next.js fetch cache the same as HTTP caching?
No. HTTP caching (via Cache-Control headers) is handled by browsers and CDNs. Next.js fetch cache is server-side, application-level caching inside Node.js. Both exist, but serve different purposes: HTTP cache reduces CDN/browser requests; Next.js cache reduces origin server work and database queries.
How do I tell if my page is cached?
In production, check the page's HTTP response headers for x-nextjs-cache or measure response time: a cached page's Time to First Byte is consistently fast (under 50 ms). In development, the cache is less reliable due to hot reload.
Can I share cache across multiple server instances?
Default Next.js cache is per-instance (in-memory). For distributed deployments, use an external cache like Redis. Next.js plans built-in distributed cache support (announced 2025); check Next.js releases for updates.
What happens to cached data if the source API changes the data schema?
Cached results won't update until the cache expires or is revalidated. If the API schema changes (e.g., field renamed), your cached page may break. This is a risk of caching: plan your revalidation strategy around known schema changes, or revalidate immediately after a schema update.
Does revalidate: 0 make a page fully dynamic with no caching?
Yes. revalidate: 0 disables application-level caching. The page is computed for each request. HTTP-level caching may still apply if your server sends Cache-Control headers, but that's separate from Next.js' fetch cache.