skip to content

Your team is upgrading a Next.js App Router codebase from Next 14, where `fetch` in Server Components was cached by default, to Next 15 or later, where it is not. How do you decide which fetches to opt back into the Data Cache?

level: principalimportance: should knowfreq 30%

answer

  1. do not blanket-restore the old default
  2. the flip is a free audit
  3. classify by audience, then by staleness
  4. opt in on evidence, not pre-emptively
  5. make intent explicit so defaults stop mattering

basics

~20 s

Do not restore the old default globally. Classify each fetch by whether its response is shared across all users and how stale it may be, opt in only the shared ones with force-cache or an interval, and treat the upgrade as the moment to make caching intent explicit.

solid answer

~50 s

The wrong move is to chase the old behaviour by blanket-adding `cache: 'force-cache'` — that reinstates an implicit policy that was already causing bugs, and it will hide the personal-data fetches that were quietly being shared. I would treat the flip as a forced audit. Classify every server `fetch` on two axes: **is this response the same for every user**, and **how stale may it be**. Shared and tolerant of staleness gets `next: { revalidate: N }` with the interval derived from how often the source actually changes. Shared and effectively immutable gets `cache: 'force-cache'`, ideally with tags so a mutation can invalidate it by name. Anything whose answer depends on who is asking stays uncached, and the new default is doing the right thing there for free. Then measure: origin request volume per endpoint before and after tells you which omissions actually mattered, and you opt those in deliberately rather than pre-emptively.

go deeper

for a junior

Know that the fetch caching default changed between Next 14 and Next 15, and that after upgrading you have to ask for caching explicitly rather than assuming it.

for a middle

Be ready to describe the two ways to opt back in — cache: 'force-cache' and next.revalidate — and to explain why applying either one everywhere is worse than deciding per fetch.

for a senior

Show that you would sequence the migration so correctness lands first and caching is restored on measured evidence, and that you can name the cross-user sharing a blanket restore would hide.

for a principal

Own the framing: the real defect is that product-visible freshness and data-sharing policy were encoded in a framework default. Set the rule, centralise data access so it is applied once per source, and make the next default change a non-event.

## Why "restore the old default" is the wrong instinct The temptation is to make the upgrade a no-op — script `cache: 'force-cache'` onto every server `fetch` and move on. It is fast, it is reversible, and it is a mistake for three reasons. First, the old default was itself a known source of bugs: content frozen at build time, dashboards showing yesterday's numbers, and teams reaching for `no-store` as a superstition rather than a decision. Reinstating it globally re-buys those bugs. Second, and more seriously, it re-buys any *cross-user* sharing that was happening implicitly. Under the old default a fetch of a per-user endpoint was cached unless someone thought to opt out. The upgrade is the one moment when that class of bug surfaces as a behaviour change you can actually observe. A blanket script buries it again. Third, it leaves the codebase with no expressed intent. Every fetch carries the same annotation, so a reader still cannot tell which data is genuinely shareable. ## The classification that drives the decision Two questions, asked per fetch: **Is this response the same for every user?** Not "is it usually the same" — is it *by construction* independent of who is asking. Catalogues, published content, reference data, feature configuration: yes. Profiles, carts, entitlements, anything the authorisation layer touches: no. **How stale may it be, from the product's point of view?** Expressed in the units the business uses — "an editor should see their change within a minute", "pricing must be correct at checkout", "this changes twice a year". That grid gives you the policy: - **Shared, immutable or invalidated on write** → `cache: 'force-cache'`, plus `next.tags` so the writing side can invalidate by name. - **Shared, tolerant of a known staleness window** → `next: { revalidate: N }`, with N derived from how often the *source* changes rather than from how fresh you want the page to feel. - **Not shared** → leave it uncached. The new default is correct here, and adding `cache: 'no-store'` is worth it as documentation on the ones a future reader might be tempted by. - **Shared but expensive and rarely changing, with no invalidation hook** → a long interval is usually better than `force-cache`, because an unbounded entry with no invalidation path is a page nobody can correct without a deploy. ## Sequencing the work A migration that tries to classify every fetch up front stalls. A more reliable order: 1. **Ship the upgrade with the new default**, changing nothing. Correctness improves immediately: personal data stops being shared, and stale-content complaints stop. 2. **Watch the origins.** Request volume per endpoint is the signal that separates fetches that genuinely needed caching from the ones nobody missed. Most codebases find the list is far shorter than the number of call sites. 3. **Opt in the hot, shared ones**, one at a time, with a stated interval and a tag. Each is a small reviewable change with a visible before/after. 4. **Write the rule down** so new code does not re-litigate it, and so "why is this cached?" has an answer other than the framework's default. Step 1 is the part that needs organisational courage: it temporarily costs origin traffic. Naming that cost in advance — with a rough estimate and a rollback plan for the two or three heaviest endpoints — is what makes it acceptable, and it is usually far cheaper than the alternative of shipping a leak. ## Guarding against the next flip The deeper lesson is that this codebase had its data-freshness policy encoded in a framework default, which is why a version bump could change product behaviour. The durable fix is to make caching intent explicit at every call site that has one, so the default becomes irrelevant. When every shareable fetch says what it is and why, the next major version's default is a footnote instead of an incident. A secondary guard is a small number of shared data-access modules rather than `fetch` scattered through components. Centralising means the classification happens once per data source instead of once per call site, and it gives you somewhere to put the tag vocabulary. ## What a strong answer sounds like Name the trap (blanket restore), give the two-axis classification, sequence the rollout so correctness lands first and performance is restored on evidence, and finish on the structural point: the goal is not to reproduce Next 14 behaviour but to stop depending on any default at all.

  • What is the risk of shipping the upgrade without opting anything back in first?
    A step change in origin traffic, concentrated on whichever endpoints were being served from cache most often. It is a capacity risk, not a correctness one, and it is manageable: estimate the heaviest few endpoints in advance, have the opt-in change ready for them, and watch request volume during the rollout. The tradeoff is worth naming explicitly, because the alternative — a blanket restore — silently keeps any cross-user sharing alive.
  • How do you choose the revalidate interval when opting a fetch back in?
    From how often the underlying source actually changes, cross-checked against what the product can tolerate. A feed updated hourly gains nothing from a ten-second interval except origin load; content edited a few times a day is fine at minutes. When the source changes unpredictably, a long interval plus tag-based invalidation on write beats guessing a number.
  • How do you stop the same problem recurring at the next major version?
    Stop depending on the default. Every fetch whose response is shareable states its caching intent explicitly, and every fetch that must not be shared says so too. Concentrating data access in a few modules helps, because the classification is then made once per data source. When intent is written down at the call site, a change of default is a release-note item rather than a behaviour change in production.

saying these in an interview costs you the question

  • Scripting force-cache onto every fetch to keep old behaviour
  • Treating the change as purely a performance regression
  • Picking revalidate intervals from how fresh the page should feel
  • Assuming every fetch that was cached before was safe to cache
  • Leaving caching policy encoded in whatever the framework default is

context