nextjs · tutorial · agents
Next 15 Fetch Lies by Default: Fix It
Next 15 ended ambient fetch caching: every unfetch-marked call goes live. Audit each fetch, mark explicitly, sample behaviorally. Fix inside.

Prices went stale and nobody told the team
Two weeks after the 15 upgrade, support wrote: "your pricing page shows last month's plans." It did — cached at some layer, serving fossils while the database held truth. Then the inverse: a dashboard hammering the database per refresh, no-store by new default, costs climbing per view.
Same upgrade, opposite failures: stale where live was needed, live where cached would do. Next 14 had cached fetches ambiently (sometimes wrongly, but predictably); Next 15 stopped, silently, everywhere at once.
Nextjs 15 fetch cache flip is the quietest breaking change in the corridor: no build errors, no red — just behavior drift across every data call, discovered by users before developers.
Ambient vs explicit caching
| Fetch | Next 14 (ambient) | Next 15 (explicit-or-live) |
|---|---|---|
Plain fetch(url) |
Cached (sometimes wrongly) | Live (always fresh, always costs) |
| Dashboard polling | Cheap (cached) | Expensive (per-view queries) |
| Pricing page | Occasionally stale | Always live (correct, costly) |
| Revalidate paths | Worked ambiently | Need explicit options |
| Developer awareness | Low (magic) | Required (no magic left) |
Three details make the flip treacherous:
- Silence is the delivery mechanism. No codemod flags it (behavior, not syntax), no build fails, no types complain — the app compiles green and behaves differently. Silent breaks bypass every gate except behavioral sampling, which nobody runs unprompted. Gates check form; flips change meaning.
- Both failure modes bill somebody. Stale pricing loses trust (and sales); live-everything dashboards bill database and compute per view. The flip doesn't pick your poison — it serves both simultaneously across different routes. Undecided fetches fail in whichever direction hurts most, per route, concurrently.
- Assumptions fossilize in code. Every
fetch()written under ambient rules encodes an expectation the new default violates — hundreds of calls, each silently redefined. Code doesn't rot; its context does. Upgrades that change defaults rewrite every line's meaning without touching a character.
The corridor post ordered the migration. This one audits its stealthiest step.
Nextjs 15 fetch cache explicitness
Inventory, decide, verify behaviorally — every fetch marked on purpose.
Step 1 — Inventory every fetch call in the app.
# the full census (10 min, non-negotiable before any other 15 work):
grep -rn "fetch(" app/ lib/ providers/ --include="*.js" --include="*.jsx"
# classify each: STATIC (same for all) / LIVE (per-user/fresh) / UNDECIDED ( shrug → decide now)Undecided is the dangerous bucket — every shrug is a future incident wearing today's deadline. Classify in one sitting with the product sense fresh: static content, user data, prices, dashboards, auth-adjacent. The inventory is the review artifact; unlisted fetches are the bug class. Census first, code second.
Step 2 — Mark each explicitly, no defaults relied upon.
// STATIC: force-cache + revalidate window (prices hourly, content daily)
fetch(url, { cache: "force-cache", next: { revalidate: 3600 } });
// LIVE: no-store, deliberately (dashboards, carts, auth-adjacent)
// const data = await fetch(url, { cache: "no-store" });Explicit marks survive future default flips (the code says what it means regardless of ambient rules) and review cleanly (a fetch without cache options fails review on sight). Revalidate windows encode staleness tolerance per route — business decisions in code, visible, adjustable. Defaults are opinions you didn't choose; marks are opinions you did.
Step 3 — Verify behaviorally, not just by build.
# per route class, with network tab open:
# STATIC: load twice → second load serves cache (fast, no DB query in logs)
# LIVE: load twice → fresh queries both times (correct, metered knowingly)
# the build passing proves nothing here — behavior is the only green that countsGreen builds certify compilation; cache behavior needs eyes on network timing plus database logs. Sample per class (not per route — classes share semantics), record expectations beside the marks (comments stating why force-cache/no-store), re-sample quarterly. Behavioral proof for behavioral changes: match the verification to the break.
That's the product — fetches marked, behavior witnessed.
Flip audit scorecard
| Census (app routes) | Before (ambient) | After (explicit) |
|---|---|---|
| Fetches inventoried | 0 (assumed fine) | 47 (all classified) |
| Undecided/hopeful | 47 (every one) | 0 |
| Stale-pricing incidents | 1 (support-found) | 0 |
| Surprise DB load | Dashboard polling spike | Budgeted, known |
| Review rule | None | Unmarked fetch fails review |
BoringPush fetches carry explicit marks, reviewed per PR.
Said without spin: explicitness costs audit afternoons (census + classification + sampling ≈ half a day) plus ongoing review friction (every new fetch needs its mark justified). Priced against stale-pricing trust loss plus surprise database bills: half a day buys behavior chosen over behavior that merely happened. Explicit costs once; ambient costs forever, randomly.
FAQ
Is nextjs 15 fetch cache change documented clearly?
Adequately in upgrade guides, invisibly in practice: developers skim breaking-change lists for red (build errors) and miss behaviorals (silent flips). The documentation exists; attention doesn't reach it. Treat every major's "behavior changes" section as mandatory reading with a checklist, not release notes to skim. Docs work when read as assignments.
Should new code avoid fetch entirely (axios, SWR, React Query)?
Orthogonal choice: the caching semantics live in Next's fetch extension (cache options, revalidation), not the transport — axios/SWR bypass Next caching entirely (client-side paradigms with their own rules). Server Components: Next fetch with explicit marks. Client interactivity: SWR/React Query with their staleness configs. Right tool per boundary, explicit everywhere, magic nowhere.
How do revalidateTag/revalidatePath fit explicit marks?
They're the invalidation half: marks declare cacheability, tags/paths declare freshness control (purge this group on mutation). Untagged force-cache is write-once (stale until window); tagged force-cache is managed (fresh on demand). Mutating routes need tags the way reads need marks — halves of one system, migrated together, audited together.
What about third-party API fetches (Stripe, Resend, CMS)?
Default no-store (external truth changes outside your control; caching others' state manufactures staleness), with short force-cache only for slow-stable endpoints (exchange rates hourly, CMS content with webhooks purging). External data obeys external clocks — cache others' state briefly or never, and purge on their webhooks where offered. Trust boundaries cache differently.
Can agents do the fetch census reliably?
Yes, with the inventory-first instruction (list all, classify, await approval before marking): agents grep tirelessly and classify decently, humans adjudicate the undecided bucket (product judgment). Census is ideal agent work (mechanical, complete); classification needs the human pass (staleness tolerance is business). Mechanical census, judgmental classification, verified sampling — split by strength.
When do fetch defaults change again — how do I future-proof?
They will (caching philosophy swings per major): explicit marks are the future-proofing (code stating intent survives default flips), plus the census habit (re-run inventory per major upgrade). Ambient reliance breaks on schedule; explicitness endures across philosophies. Write marks like defaults will betray you — because eventually, one will.
Keep reading
- 14 to 16 Upgrade: Caching Broke Everything — the corridor this audit slots into
- Next 16 Async Params Broke My Build: Fix — step zero of the same migration
- Router Cache Confusing You? Next.js Caching, Fixed — the cache mental model underneath
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


