Data Fetching in Next.js: fetch(), Caching & Revalidation
You've now seen SSG, SSR, and ISR as separate rendering strategies, and you've used fetch() with options like revalidate and cache: 'no-store' along the way. This lesson pulls that thread together explicitly: in the Next.js App Router, your data fetching choices and your rendering strategy are not two separate decisions — they're deeply connected. The caching option you choose on a fetch call often directly determines whether a route ends up statically or dynamically rendered.
Next.js extends the standard, browser-native fetch() API with additional caching and revalidation options specifically designed to work with its rendering system. Understanding exactly what force-cache, no-store, and the revalidate option each do — and how they interact — is essential for making deliberate, correct rendering decisions rather than being surprised by which strategy Next.js chose for a given page.
Learning Objectives
- Understand how Next.js extends the standard fetch() API with caching options.
- Explain the difference between cache: 'force-cache' and cache: 'no-store'.
- Use the revalidate option to control time-based cache freshness per fetch call.
- Recognize how fetch caching choices influence a route's overall rendering strategy.
- Choose the correct caching strategy for a given data source and use case.
Core Definitions
- Data Cache: Next.js's persistent server-side cache for fetch() results, which can survive across multiple requests and even multiple deployments if unchanged.
- cache: 'force-cache': A fetch option telling Next.js to cache this request's result indefinitely (subject to the Data Cache) and reuse it for future requests without refetching, supporting static rendering.
- cache: 'no-store': A fetch option telling Next.js to never cache this request's result, fetching fresh data on every single call and forcing dynamic rendering for that route.
- next: { revalidate: N }: A fetch option combining caching with automatic background regeneration after N seconds, enabling ISR behavior for that specific data.
- Request memoization: A separate, short-lived optimization where Next.js deduplicates identical fetch calls made multiple times within the same single render pass.
Detailed Explanation
Next.js's fetch() is the same function browsers and Node.js already provide, extended with extra configuration recognized specifically by Next.js's caching system when called inside a Server Component. This is a deliberate design choice: you don't need a special Next.js-specific data-fetching function; you use the fetch() you already know, with a few additional options layered on top.
The cache option is the most fundamental choice. Setting `cache: 'force-cache'` (which, notably, is the default behavior for fetch calls in Server Components unless you specify otherwise) tells Next.js to cache this request's result in its Data Cache and reuse that cached value for future requests and future visitors, without re-fetching, until something explicitly invalidates it. This caching behavior is exactly what enables a route to be statically rendered — since the data doesn't need to be fetched fresh per visitor, the resulting HTML can be built once and reused.
Setting `cache: 'no-store'` does the opposite: it tells Next.js to never cache this particular fetch's result, fetching completely fresh data on every single call. Using this option on even one fetch call within a route is enough to make that entire route render dynamically (SSR), since Next.js can no longer guarantee the page's output is the same for every visitor.
The middle ground, which you saw in the ISR lesson, is `next: { revalidate: N }`. This tells Next.js to cache the result (similar to force-cache) but automatically treat it as stale after N seconds, triggering the stale-while-revalidate background regeneration process. This is what gives a route ISR behavior rather than pure, permanent SSG or fully dynamic SSR.
It's worth distinguishing this Data Cache behavior from a separate, related optimization called request memoization: within a single render pass (rendering one page for one specific request), if multiple different components each call fetch() with the exact same URL and options, Next.js deduplicates those calls, only actually executing the network request once and reusing the result for all callers during that render. This is automatic and requires no configuration, but it only lasts for the duration of a single render — it's not the same as the longer-lived, cross-request Data Cache controlled by the cache and revalidate options.
Understanding these three fetch behaviors — force-cache (default, enables static rendering), no-store (forces dynamic rendering), and revalidate (enables ISR) — gives you precise, deliberate control over exactly how each piece of data in your application is fetched, cached, and how that choice ripples out to determine your route's overall rendering strategy.
Diagram Description
Visualize how fetch cache options map to rendering strategy:
fetch(url) → cache: 'force-cache' (default) → Contributes to STATIC rendering (SSG)
fetch(url, { cache: 'no-store' }) → Never cached, always fresh → Forces DYNAMIC rendering (SSR)
fetch(url, { next: { revalidate: 60 }}) → Cached, refreshed every 60s → Enables ISR behavior
Within ONE render pass:
[ComponentA] fetch('/api/user') ─┐
[ComponentB] fetch('/api/user') ─┼── same URL+options → Next.js sends ONE actual network request (request memoization)
[ComponentC] fetch('/api/user') ─┘
Comparison Table
| Fetch Option | Caching Behavior | Rendering Impact |
|---|---|---|
| cache: 'force-cache' (default) | Cached indefinitely in the Data Cache | Supports static rendering (SSG) |
| cache: 'no-store' | Never cached; fresh on every call | Forces dynamic rendering (SSR) |
| next: { revalidate: N } | Cached, but regenerated after N seconds | Enables ISR (stale-while-revalidate) |
| Request memoization (automatic) | Deduplicates identical calls within one render | No direct rendering impact; a performance optimization only |
Next.js Practical Example
// Three different caching strategies within the same app
// 1) STATIC — cached indefinitely, contributes to SSG (this is the default)
async function getCompanyInfo() {
const res = await fetch('https://api.example.com/company', {
cache: 'force-cache', // explicit here, but this is the default anyway
});
return res.json();
}
// 2) DYNAMIC — never cached, forces SSR for the route using this
async function getLiveStockPrice(ticker: string) {
const res = await fetch(`https://api.example.com/stocks/${ticker}`, {
cache: 'no-store',
});
return res.json();
}
// 3) ISR — cached, but auto-refreshed every 5 minutes
async function getBlogPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 300 },
});
return res.json();
}
getCompanyInfo uses force-cache (the default), meaning this data is fetched once and reused across every visitor and every future request until something explicitly invalidates it — ideal for content like a company's 'About Us' details that essentially never changes. getLiveStockPrice uses no-store, guaranteeing a completely fresh network request every single time it's called, which is essential for genuinely real-time data like a stock price — any route that calls this function will automatically become dynamically rendered. getBlogPosts uses revalidate: 300, caching the blog list for up to five minutes at a time before automatically regenerating it in the background, striking a balance between the performance of caching and reasonably fresh content, without needing full SSR on every request.
Industry Examples
- E-commerce sites use force-cache for rarely-changing content like category descriptions, no-store for live inventory counts on checkout pages, and revalidate for product listings that update periodically.
- News platforms use no-store for a live 'breaking news' ticker component while using revalidate with a short window for the rest of a homepage's article grid.
- Financial dashboards use no-store extensively for account balances and market data, accepting the higher per-request server cost in exchange for guaranteed accuracy.
- Documentation sites use force-cache (the default) almost everywhere, since documentation content changes infrequently and benefits enormously from static caching and CDN delivery.
- Multi-vendor marketplaces use revalidateTag alongside next: { revalidate } and tags on fetch calls, so that a single vendor updating their listing can trigger an immediate refresh across every page displaying that listing's data.
Interview Questions and Answers
Q1. What is the default caching behavior of fetch() inside a Next.js Server Component?
By default, fetch() behaves as if cache: 'force-cache' were set, meaning the result is cached in Next.js's Data Cache and reused across requests, supporting static rendering unless a different caching option is explicitly specified.
Q2. What does cache: 'no-store' do, and how does it affect a route's rendering strategy?
cache: 'no-store' tells Next.js to never cache that particular fetch's result, always fetching fresh data on every call. Using it anywhere in a route forces that entire route to render dynamically (SSR), since the page's output can no longer be guaranteed identical across requests.
Q3. How does next: { revalidate: N } differ from both force-cache and no-store?
It combines caching with automatic time-based invalidation: the result is cached and reused (like force-cache) but is automatically considered stale after N seconds, triggering background regeneration (stale-while-revalidate) rather than either permanent caching or no caching at all.
Q4. What is request memoization and how is it different from the Data Cache?
Request memoization automatically deduplicates identical fetch calls (same URL and options) made by different components within a single render pass, so the actual network request only happens once. This is different from the Data Cache, which persists across multiple requests and even deployments, controlled explicitly by the cache and revalidate options.
Q5. If a route contains multiple fetch calls with different caching options, how does Next.js determine its overall rendering strategy?
If even one fetch call in that route uses cache: 'no-store' (or another dynamic API is used), the entire route becomes dynamically rendered. A route is only eligible for full static rendering if none of its data sources require per-request freshness.
MCQs With Answers
1. What is the default cache behavior of fetch() in a Next.js Server Component?
- no-store
- force-cache
- no-cache
- revalidate: 0
Answer: B. force-cache
Explanation: Unless specified otherwise, fetch() in a Server Component defaults to force-cache behavior, caching the result and supporting static rendering.
2. Which fetch option forces a route to render dynamically (SSR)?
- cache: 'force-cache'
- next: { revalidate: 3600 }
- cache: 'no-store'
- No option is needed; it's automatic
Answer: C. cache: 'no-store'
Explanation: no-store tells Next.js to never cache the result and always fetch fresh data, which forces the entire route to render dynamically per request.
3. What does next: { revalidate: 120 } enable?
- Permanent caching with no updates ever
- ISR behavior: cached data refreshed at most every 120 seconds
- Immediate, per-request fresh data
- Deleting the cached data after 120 seconds with no replacement
Answer: B. ISR behavior: cached data refreshed at most every 120 seconds
Explanation: This option caches the fetch result but automatically triggers background regeneration once 120 seconds have passed, following the stale-while-revalidate pattern.
4. What is request memoization?
- A way to permanently cache data across all users forever
- Automatic deduplication of identical fetch calls within a single render pass
- A method for compressing fetch responses
- A security feature for API keys
Answer: B. Automatic deduplication of identical fetch calls within a single render pass
Explanation: If multiple components call fetch() with the same URL and options during one render, Next.js automatically executes the network request only once and shares the result, without any manual configuration.
5. If a route has 5 fetch calls, 4 using force-cache and 1 using no-store, how is the route rendered?
- Statically, since most calls use force-cache
- Dynamically, because even one no-store call forces dynamic rendering
- It fails to build
- Half static, half dynamic
Answer: B. Dynamically, because even one no-store call forces dynamic rendering
Explanation: A single fetch call using no-store is enough to make Next.js treat the entire route as needing dynamic, per-request rendering.
Common Mistakes to Avoid
- Assuming fetch() in Next.js behaves identically to plain browser fetch() with no caching, when it actually defaults to force-cache behavior in Server Components.
- Using cache: 'no-store' on every fetch call out of caution, unnecessarily forcing entire routes into full SSR when ISR or static caching would have been sufficient.
- Confusing request memoization (automatic, single-render deduplication) with the longer-lived Data Cache controlled by cache and revalidate options.
- Forgetting that a single no-store or dynamic-API-using fetch call anywhere in a route is enough to make the entire route dynamic, not just that one fetch's data.
- Not adding tags to fetch calls when planning to use revalidateTag later, making targeted on-demand revalidation impossible without a broader refresh.
Interview Notes
- fetch() in Server Components defaults to cache: 'force-cache' behavior unless configured otherwise.
- cache: 'no-store' disables caching entirely, forcing fresh data every call and dynamic rendering for that route.
- next: { revalidate: N } caches data but refreshes it in the background after N seconds, enabling ISR.
- Request memoization automatically deduplicates identical fetch calls within a single render pass, separate from the longer-lived Data Cache.
- A route's overall rendering strategy is determined by the most 'dynamic' data requirement among all its fetch calls and API usage.
Key Takeaways
- Next.js deliberately builds on the standard fetch() API rather than inventing a separate data-fetching function, extending it with caching options instead.
- The cache and revalidate options aren't just about performance — they directly determine whether a route is statically or dynamically rendered.
- Choosing the right caching strategy per data source, rather than defaulting to the same option everywhere, is key to building both fast and appropriately fresh applications.
- Request memoization is a helpful automatic optimization but is conceptually distinct from the deliberate, configurable Data Cache.
Summary
Next.js extends the standard fetch() API with caching options that directly determine a route's rendering strategy. The default behavior, equivalent to cache: 'force-cache', caches results in Next.js's Data Cache and supports static rendering. Explicitly setting cache: 'no-store' disables caching entirely, forcing fresh data on every call and making the entire route render dynamically (SSR). The middle-ground option, next: { revalidate: N }, caches data while automatically refreshing it in the background after N seconds, enabling ISR behavior. Separately, Next.js automatically deduplicates identical fetch calls made within a single render pass through request memoization, a short-lived optimization distinct from the longer-lived, explicitly configured Data Cache. Together, these options give precise, deliberate control over both performance and freshness on a per-data-source basis.