Next.js 16 Fetch Caching Changed: Why Your Data Stopped Updating (or Started Refetching Too Much)
Two very different bug reports point at the same root cause after a Next.js 16 upgrade: “my dashboard is showing stale data that never updates” and “my dashboard is hammering the database on every request now.” Both usually trace back to the same change — fetch() is no longer cached by default inside Route Handlers, and the assumptions your code made about caching behavior under the old default may now be exactly backwards.
What the old default actually did
In earlier Next.js versions, fetch() calls inside Route Handlers were cached by default, unless you explicitly opted out. That default made sense for a lot of static and semi-static content, but it also caused a well-known class of bugs — developers building what they assumed was a dynamic API endpoint, not realizing responses were being cached and served stale until they explicitly disabled it.
Get Syamkumar’s stories in your inbox
Join Medium for free to get updates from this writer.
Next.js 16 flips the default: fetch() is no longer cached automatically. If you want caching, you now opt in explicitly.
The fix, both directions
If you genuinely want caching (and for a lot of read-heavy dashboard data, you do):
const data = await fetch("https://api.example.com/stats", {
next: { revalidate: 60 }, // cache for 60 seconds
});If you were relying on the old implicit caching and didn’t realize it, you now need to decide, per endpoint, whether caching is actually appropriate — and add it explicitly where it is. The dangerous version of this bug isn’t the one where data goes stale; it’s the reverse, where an app that used to serve cached responses now hits your database or a rate-limited third-party API on every single request, because nobody added revalidate back in.
Where this actually bites teams
The two failure modes I see most:
- A dashboard analytics endpoint that used to be implicitly cached starts making a fresh, expensive aggregation query on every page load post-upgrade — invisible until either your database load spikes or your third-party API bill does.
- A “live” data view that was accidentally cached under the old default finally starts updating in real time after the upgrade, and someone reports it as a bug because they’d built their mental model around the old (arguably wrong) cached behavior.
The second case is a good reminder that this change is fixing a real footgun, even though it costs you an audit pass to get there.
Audit every fetch call, not just the ones you remember
The practical fix here isn’t clever — it’s thorough. Go through every fetch() call inside your Route Handlers and Server Components and make an explicit decision for each one:
- Should this be cached? If yes, add
next: { revalidate: N }with a sensible interval for how fresh the data needs to be. - Does this need to always be fresh? Leave it uncached (the new default), or be explicit with
cache: "no-store"so the intent is clear to the next person reading the code. - Is this being called from a client-triggered action, like a form submission? Those almost always want fresh data — the new default is actually correct for them without any change needed.
Writing the intent down explicitly, even where the new default already matches what you want, is worth doing anyway — it means the next developer (or you, in six months) doesn’t have to reverse-engineer whether the missing revalidate was a deliberate choice or an oversight.
Route segment config still matters, separately
Don’t confuse this with the export const revalidate route segment config, which controls page/segment-level caching and works differently from the per-fetch option. Both exist, both matter, and mixing them up is a common source of "I set revalidate and nothing changed" confusion — the segment-level setting and the fetch-level option can override each other depending on which is more specific, so it's worth understanding which one you're actually configuring.
Why this is easy to get wrong under deadline pressure
An audit like this is exactly the kind of unglamorous work that gets skipped when a team is focused on shipping features, and it’s also exactly the kind of thing that turns into a 2am incident when a cached endpoint that should’ve been live silently served stale billing data, or an uncached one that should’ve been cached quietly ran up an API bill. It’s the sort of infrastructure decision that’s worth having made correctly and deliberately from the start rather than inherited from a default you didn’t know existed.
That’s the standard the data layer in the Ovyqen Next.js SaaS dashboard kit is built to — every fetch and query has an explicit, documented caching decision behind it, tuned for dashboard workloads specifically, instead of inheriting whatever the framework’s default happened to be that release. If you’re comparing next.js templates and this is the kind of detail you’d rather not audit yourself across a whole app, it’s already handled there.
Quick answers
Does this affect Server Component fetches the same way as Route Handlers? The default caching behavior for fetch() in Server Components is governed by the same underlying rules — the safest approach is to treat every fetch call as needing an explicit caching decision, regardless of where it lives.
How do I know if I was relying on the old default without realizing it? Audit for fetch() calls with no cache or next.revalidate option set — anywhere caching mattered for that endpoint under the old behavior, it needs an explicit decision now.
Is there a way to restore the old global default? No — this is a deliberate default change, not a flag. Per-request explicitness is the new model, and honestly, the better one.
This isn’t a hard bug to fix once you know it exists. The risk is entirely in not knowing it exists until a customer notices first.










