skip to content

A Next.js App Router dashboard showed one stale number, and a teammate fixed it by adding `export const dynamic = 'force-dynamic'` to the page — but only one of that page's six data reads has to be fresh. What narrower opt-outs were available, and what did the broad fix cost?

level: middleimportance: should knowfreq 52%

answer

  1. narrowest lever that fixes it
  2. per read, per component, per segment
  3. one stale number, six re-fetched
  4. prerendered artifact is what you lose
  5. isolate the fresh part instead

basics

~20 s

Opt out at the narrowest scope that fixes the bug: no-store on the one fetch, or connection() in the one component that must run per request. Forcing the whole segment dynamic re-runs all six reads on every request and ends prerendering for the route.

solid answer

~50 s

Next gives you opt-out levers at three different scopes, and the teammate reached for the widest one. At **one read**, `fetch(url, { cache: 'no-store' })` keeps that response out of the Data Cache. At **one component**, `await connection()` marks everything after it as request-time work. At **the whole segment**, `export const dynamic = 'force-dynamic'` or `export const revalidate = 0` opts the route out of prerendering entirely — and `export const fetchCache = 'force-no-store'` does the same to every fetch in it. Picking the segment lever for one stale number means the other five reads also re-execute per request and the route stops being served from a prerendered artifact, so TTFB is now the sum of that data latency and origin load scales with traffic. The better shape is to keep the route prerenderable and isolate the fresh piece into its own component so only that subtree is request-time work. Since Next 15, caching a read and choosing a rendering mode are separate decisions — make both explicitly.

code

tsx · 17 lines
tsx
// app/dashboard/page.tsx — narrow opt-out, no segment-level flag
export default async function Page() {
  const config = await fetch('https://api.example.com/config', {
    cache: 'force-cache',
  }).then((r) => r.text())

  const live = await fetch('https://api.example.com/ticker', {
    cache: 'no-store',
  }).then((r) => r.text())

  return (
    <main>
      <pre>{config}</pre>
      <pre>{live}</pre>
    </main>
  )
}

go deeper

for a junior

Know that there is more than one place to turn caching off — on a single fetch call, and on the route segment as a whole — and that the single fetch is the smaller change.

for a middle

Be able to lay out the ladder from per-read to per-component to per-segment, say what each rung stops, and explain why the segment-wide flag also re-runs the five reads that were fine.

for a senior

Show the diagnosis: identify which read was stale, argue why the narrow opt-out was or was not sufficient, and quantify what the wide one costs in TTFB and origin load before you accept it.

for a principal

Frame it as a standard: opt-outs are stated at the smallest scope, never buried in shared helpers, and a segment-wide dynamic flag is an explicit documented decision with a capacity implication attached.

## Three scopes, three levers The useful mental model is a ladder from narrow to wide. Every rung is a legitimate opt-out; the craft is picking the lowest one that actually fixes the symptom. **1. One data read.** Pass the cache option on the individual call: ```ts const res = await fetch('https://api.example.com/ticker', { cache: 'no-store' }) ``` `'no-store'` means the response is not written to, or read from, Next's persistent Data Cache. Nothing else about the route changes. **2. One component or one region of the render.** `connection()` from `next/server`, awaited inside a server component, marks the point after which work must happen at request time: ```tsx import { connection } from 'next/server' export default async function LiveBadge() { await connection() return <span>{new Date().toISOString()}</span> } ``` **3. The whole route segment.** Route Segment Config exports in `page.tsx`, `layout.tsx`, or `route.ts`: ```ts export const dynamic = 'force-dynamic' // never prerender this segment export const revalidate = 0 // do not keep a stored render of it export const fetchCache = 'force-no-store' // every fetch in the segment uncached ``` `dynamic = 'force-dynamic'` and `revalidate = 0` both land in the same place — the segment renders per request — and `fetchCache` is the rarely-needed sledgehammer for the data side. ## Why scope is the whole question The stale number was one read. Reaching for rung 3 changed the behaviour of all six. Concretely, the page loses: - **Prerendered output.** The route can no longer be served from a stored render, so a CDN in front of you has nothing to replay and every request lands on the origin. - **Parallel-free latency.** TTFB becomes however long the *slowest* of the six reads takes, on every single request, rather than being paid once ahead of time. - **Capacity headroom.** Origin cost now scales with request volume instead of with how often the content changes. A page that survived a traffic spike as a static artifact will not survive it as a per-request render. Meanwhile the five reads that were perfectly happy cached — the nav config, the copy blocks, the currency table — are re-fetched forever, adding load to upstream services that had no problem. ## The better shape Keep the route prerenderable and push the request-time work into its own component. The five stable reads stay in the page; the one live read moves into a small server component that is the only thing forced to run per request, streamed into the page rather than blocking the whole document. You get a fast document that is mostly cacheable and one region that is genuinely fresh. That is the answer interviewers are fishing for: not "which flag makes it fresh" but "what is the smallest thing I can make dynamic." ## Rendering mode and data caching are separate decisions Since Next 15, an uncached `fetch` is about *storage* — it says the response must not be read from or written to the Data Cache. It is not the lever you should lean on to guarantee that your component *executes* per request. If the route is prerendered, the code around that fetch still runs ahead of the user. So when a page must reflect request-time reality, make the rendering choice explicit — `await connection()` for a component, the segment config for a whole route — and make the storage choice explicit on each read. Two decisions, stated separately, in the same change. This discipline is also the version-proof one. Next's *defaults* around fetch caching have moved between major versions and are a classic source of stale interview answers; what has not moved is that you can always name the storage behaviour on the call and the rendering behaviour on the segment. ## How to talk about the diagnosis A strong answer walks the ladder out loud: which read was stale, whether that read alone could be marked uncached, whether the component around it also needed to run per request, and only then whether the whole segment had to go dynamic. Finish with the cost sentence — "this route now costs a server render per request, so I want that to be true for one component rather than for six reads." That is the sentence that turns a config recital into an engineering answer.

  • Is there a difference between `export const revalidate = 0` and `export const dynamic = 'force-dynamic'`?
    They arrive at the same outcome from different angles. `revalidate = 0` says no stored render of this segment may be reused, and `dynamic = 'force-dynamic'` says do not prerender it at all. Either way the segment renders per request. `force-dynamic` states the intent more directly, so prefer it when the intent is "this route is request-time"; use `revalidate` when you are expressing a lifetime.
  • If the fresh read lives in a shared helper imported by every page, what changes?
    The narrow lever stops being narrow. A `no-store` inside a shared helper follows every importer, so pages that were fine lose their cached copy of that data too. Either take the cache option as a parameter so each caller states its own need, or keep two call sites. Opt-outs buried in shared utilities are how a whole app quietly stops caching.
  • Why is streaming the fresh region often better than making the whole page dynamic?
    Because the document itself can still come from a prerendered render while the one request-time region is filled in separately. The user gets bytes immediately instead of waiting on the slowest read, and your origin only does the small piece of work that genuinely could not be done ahead of time.

saying these in an interview costs you the question

  • Reaches for force-dynamic as the default fix for any staleness
  • Thinks an uncached fetch by itself guarantees per-request rendering
  • Assumes only the stale read re-runs after a segment-wide opt-out
  • Cannot name any lever narrower than the segment config
  • Puts a no-store into a shared helper used by every page

context