After a stale-pricing incident, a team adopts a rule that every Next.js App Router route carries `export const dynamic = 'force-dynamic'` so nothing can ever be stale. How would you evaluate that policy?
answer
- would it have prevented the incident?
- request-time is not the same as fresh
- capacity scales with views, not changes
- classify the data, not the routes
- a blanket flag destroys information
basics
~20 sThe rule buys less correctness than it looks like and costs more than the team expects: rendering per request does not guarantee a user sees fresh data, while every route now pays a server render and origin capacity scales with traffic instead of with content change.
solid answer
~50 sI would challenge both halves of it. On correctness: rendering per request removes one source of staleness but does not make data current — a cache in front of the app, a replica lag, or a read that races a mutation all survive the change, so the incident may not even be prevented. On cost: prerendered routes are served from a stored artifact, and giving that up everywhere means origin capacity now scales with request volume, TTFB becomes the latency of the slowest read on every request, and upstream services take traffic proportional to page views. The better policy is tiered: classify data by how wrong it is allowed to be, make correctness-critical reads explicitly uncached or invalidate them on write, and let everything else stay prerendered. A blanket flag is also a governance smell — it hides which routes genuinely need request-time rendering, so the next incident gets no signal from it.
go deeper
Understand that turning caching off everywhere is a trade, not a free safety measure: every page then costs a server render, and that cost grows with the number of visitors.
Be able to separate the two claims — that dynamic rendering guarantees freshness, and that it is cheap — and explain concretely why each is weaker than it sounds.
Bring evidence: identify which layer actually served the stale value in the incident, then quantify the policy's cost in TTFB, origin CPU, and upstream request volume before recommending a change.
Own the standard: tier data by tolerated staleness, put freshness guarantees on the write path, require each remaining opt-out to be justified, and give the policy an exit condition and a visible metric.
## Start by testing whether the rule prevents the incident The most useful question to ask is not "is caching bad?" but "would this policy have prevented last week?" Frequently it would not. Rendering per request removes exactly one link in the chain — the reuse of a stored render. It does nothing about: - a shared cache or CDN in front of the app holding the HTML, - a read replica that lags the write, - a browser holding an already-loaded page open, - or a read that simply raced the write by a hundred milliseconds. If the pricing incident came from any of those, the team has paid a permanent cost for an outcome it did not buy. Establishing the actual failure path first is the difference between an engineering decision and a reflex. ## Then price what it costs A prerendered route is served from an artifact. A request-time route is a server render. Turning the first into the second, everywhere, changes the cost model in three ways worth naming out loud: 1. **Capacity scales with traffic, not with change.** A page updated twice a day used to cost two renders a day; now it costs one per view. Peak traffic becomes peak compute, and the safety margin a static site gave you during a spike is gone. 2. **Latency becomes data latency.** TTFB is now the slowest read on the page, on every request, for every user. That shows up in Core Web Vitals and, on commerce sites, in conversion. 3. **Upstream load multiplies.** Every downstream service and database behind those reads now receives traffic proportional to page views. The blast radius of a slow dependency grows from "the page is stale" to "the page is down." None of these are hypothetical, and all three are measurable before and after — which is how you turn the argument from taste into evidence. ## Propose the alternative: tiers, not a global flag The durable version of the policy classifies data rather than routes: - **Correctness-critical** — prices, balances, entitlements, anything where being wrong is an incident. These reads are explicitly uncached, or are invalidated as part of the write that changes them, so freshness is a property of the mutation path rather than of a rendering flag. - **Freshness-sensitive but tolerant** — inventory counts, dashboards. A bounded lifetime is fine and should be written down as a number the business agreed to. - **Effectively static** — marketing copy, docs, navigation. Prerendered, and nobody should have to think about it again. The route-level decision then falls out of the data: a route whose every read is critical is legitimately request-time, and a route with one critical read isolates that read into its own component so the rest of the page keeps its prerendered output. The flag stops being a policy and goes back to being a tool. ## The governance argument A blanket rule destroys information. When every route carries the same opt-out, you can no longer tell which routes have a real request-time requirement, so the next engineer cannot safely remove it from any of them, and the next incident review gets no signal from the codebase. Opt-outs are valuable precisely because they are rare and deliberate: each one should be traceable to a stated reason. There is also an organisational reading. Blanket "never cache" rules almost always follow an incident where nobody could explain *why* the data was stale. The real gap is observability and ownership, not caching. Fixing that — knowing which layer served the stale bytes, and who owns invalidation on the write path — is what stops the second incident. A flag that makes the system uniformly expensive mostly buys the feeling of having acted. ## What I would actually do Run the policy as an experiment with an exit condition. Keep the blanket flag for a short, agreed window while the failure path is established and the tiering is written. Instrument origin CPU, p75 TTFB, and upstream request volume for the before/after comparison. Then remove the flag route by route, starting with the ones that read nothing critical, and require any route that keeps it to name the read that justifies it in a comment or an ADR. Add a build-time check that reports how many routes are request-time, so the number is visible and moves in a direction someone chose. The short version for an interview: the rule optimises a single failure mode at the expense of every other property of the system, on the assumption — usually untested — that request-time rendering equals correct data. Test that assumption, price the alternative, and replace the blanket with tiers.
- What evidence would you gather before making the argument to the team?The actual failure path from the incident — which layer served the stale value — plus before/after numbers for p75 TTFB, origin CPU per request, and upstream request volume. If the stale bytes came from a CDN or a lagging replica, the policy provably did not address the cause, and that single fact usually carries the discussion better than any argument about cost.
- Are there routes where you would keep force-dynamic permanently?Yes — routes that are per-user or per-request end to end, where there is no shared render worth storing: an account page, a session-scoped checkout step, an authenticated dashboard. The test is whether any two users could be served the same bytes. When the answer is no, prerendering has nothing to offer and the flag is honest rather than defensive.
- How do you keep the freshness guarantee once caching comes back?Move the guarantee onto the write path rather than the read path: the mutation that changes a price is responsible for invalidating what depends on it, and that dependency is owned and tested. Reads then stay cheap by default. Freshness enforced at read time is a per-request tax; freshness enforced at write time is paid once per change.
- How would you make the policy self-correcting rather than permanent?Attach an exit condition and a visible metric — a build-time count of request-time routes, reviewed regularly, with each remaining one justified in writing. Policies adopted during an incident survive by inertia; the only reliable antidote is making the number visible and assigning someone to move it.
saying these in an interview costs you the question
- Assumes rendering per request always means fresh data
- Never asks what actually served the stale value
- Treats caching as an optimisation rather than a capacity decision
- Argues from principle with no before/after measurements
- Keeps a blanket flag with no exit condition or owner