I spent a miserable afternoon a while back debugging a production issue where a user's private financial dashboard was showing cached data from a completely different account. Why? Because Next.js 14 decided that a standard, innocent-looking fetch() call should be cached aggressively until the end of time unless you explicitly went out of your way to block it.
It was the ultimate frontend footgun.
Thankfully, the ecosystem listened. With the recent maturity and widespread production adoption of Next.js 15 and the latest 16.x patches, the Vercel team completely flipped the script. Caching is now opt-in by default.
If you haven't audited your full-stack data fetching patterns lately, the era of pulling your hair out over stale data is overβbut it means you need to adjust how you write your data layer today.
The Nightmare of "Everything is Cached"
In previous versions, Next.js tried to be clever. It assumed that if you were making a GET request, you wanted it to be as fast as possible, so it statically cached the result at build time or on the first request.
While that worked great for basic blogs, it was a disaster for real-world dynamic apps. You had to constantly litter your code with things like:
// The old boilerplate dance just to keep data fresh
export const revalidate = 0;
// or
fetch('[https://api.example.com/data](https://api.example.com/data)', { cache: 'no-store' });
If you forgot that single configuration object on an API route handler, your dynamic endpoint was suddenly serving stale data to your users, and your serverless functions were returning zombie responses.
The Modern Fix: Uncached by Default
In the current Next.js architecture, fetch requests and GET Route Handlers are uncached by default.
If you don't explicitly ask for caching, Next.js treats your request as dynamic. Your database gets queried when the user requests the page, just like traditional backend frameworks (like Node/Express, Rails, or Laravel) have done for decades.
// How we write data fetching now (Next.js 15/16 baseline)
// This hits the API live on every single request automatically.
const res = await fetch('[https://api.yourbackend.com/users/me](https://api.yourbackend.com/users/me)');
const user = await res.json();
If you actually want to cache a heavy public payload (like a product list or a public configuration), you now have to be intentional about it:
// Explicitly opting into performance optimization
const staticData = await fetch('[https://api.yourbackend.com/products](https://api.yourbackend.com/products)', {
cache: 'force-cache'
});
This simple swap dramatically lowers the cognitive load of shipping full-stack apps. You optimize for correctness first, then opt into performance later.
Watch Out for the Asynchronous Request APIs
The caching overhaul also brought another massive structural change that catches developers off guard when upgrading: request-scoped metadata APIs are now completely asynchronous.
If you are reading cookies, headers, or route params inside a Server Component, you can no longer access them synchronously.
// β This will throw an error in modern Next.js
const cookieStore = cookies();
const token = cookieStore.get('session');
// The right way
const cookieStore = await cookies();
const token = cookieStore.get('session');
Why did they do this? To make Partial Prerendering (PPR) actually work. By making these APIs return a promise, the framework can instantly serve a static HTML shell of your page while waiting for the request-dependent dynamic parts (like user sessions or headers) to resolve and stream into the client.
Over to you
Did you get bitten by the old aggressive caching model, or did you actually prefer things being fast by default? Let's talk in the comments.
Connect with Me
If you found this guide helpful, let's connect and discuss modern development workflows!
- π» GitHub: johnnylemonny
- βοΈ DEV.to: johnnylemonny
This article was created with the help of AI
Top comments (9)
I might be reading the new model wrong, but does uncached by default also mean dynamic by default? My understanding is that a page which never touches cookies, headers or other request APIs can still be statically prerendered, and then that innocent looking fetch runs once at build time and its result gets baked into the HTML anyway. If that's the case, someone could audit their fetch calls, find no caching config anywhere, and still ship a page that serves the same data until the next deploy. Would love to know if you hit that distinction while writing this up, because it's the part of the caching story that still trips people after the flip.
You just hit the absolute nail on the head, and honestly, this is the exact nuance that is still breaking people's brains after the switch.
You are 100% correct. "Uncached by default" for a
fetchcall does not automatically mean the route becomes dynamic by default.If a page has a plain
fetch()with no configuration, and the route doesn't touch any request-time APIs (likecookies(),headers(), orsearchParams), Next.js looks at it duringnext buildand says: "Cool, nothing dynamic here, letβs statically prerender this page."When it prerenders, that "uncached" fetch fires exactly once - at build time. The resulting data gets baked straight into the static HTML/RSC payload and sits there frozen until your next deployment.
It creates a really weird paradox where the fetch itself isn't stored in the Data Cache, but the output of the fetch is trapped inside the Full Route Cache.
This is exactly why so many devs audit their data fetching, think they are safe because they aren't caching the data, and then realize their production build is acting like a static snapshot. If you want that route to hit the database on every single request without using headers or cookies, you still have to explicitly slap
export const dynamic = 'force-dynamic'at the top of the file (or rely on the newerdynamicIOflags).Seriously good call-out. It is a massive footgun that definitely trips people up after the flip.
Thanks for sharing!
You're welcome!
How does 'uncached by default' impact server-side rendering in Next.js, and are there any edge cases to watch out for? I'd love to swap ideas on this, following your posts for more insights on optimizing performance.
Glad you're finding the posts helpful! The biggest shift with "uncached by default" is that you stop serving stale data, which is a massive win for reliability.
The main trade-off is server load. Since you're hitting your DB or API on every request, you need to watch out for rate-limiting and slow downstream services. If your backend isn't optimized for that traffic, you'll see latency spikes. Itβs definitely a change from optimizing for cache hits to optimizing for backend throughput.
The cross-user dashboard example is exactly why the old caching default felt dangerous in app-router systems that handle auth-sensitive data. A framework optimization stops being a convenience pretty quickly when the failure mode is serving the wrong tenant's state from a path that looked innocuous in review. I like that you framed the shift as a data-layer rewrite, not just a version bump, because βuncached by defaultβ changes how teams should reason about fetch boundaries, revalidation, and where they intentionally reintroduce caching. In practice Iβve found the safer pattern is to treat caching as a policy decision near each data source rather than something inferred globally from the request shape. Curious whether the teams youβve worked with are now overcorrecting toward no caching at all, or actually getting more explicit and disciplined about where they opt back in.
Thatβs a spot-on observation. The failure mode of "leaking the wrong tenant's state" is exactly what made so many of us lose sleep under the old caching defaults. It really wasn't just a version bump. It was an architectural reset.
Regarding your question: Iβm definitely seeing some fear-driven overcorrection in the short term. Teams burned by previous issues are leaning hard into
no-storeacross the board just for peace of mind. But you're right. The more mature teams are moving toward exactly that "caching as a policy" model you mentioned.Instead of relying on global inference, we're seeing more explicit
revalidateTagstrategies or dedicated service layers that define cache TTLs closer to the data fetching logic. Itβs definitely making for more disciplined codebases, even if it adds a bit more upfront design work. It feels like the industry is finally treating caching as an explicit performance choice rather than a framework side-effect.Uncached by default feels much more intuitive. Fetch fresh data first, then add caching only when needed.