DEV Community

Cover image for Next.js Finally Fixed Caching (And My Database Is Sighing in Relief)

Next.js Finally Fixed Caching (And My Database Is Sighing in Relief)

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' });

Enter fullscreen mode Exit fullscreen mode

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();

Enter fullscreen mode Exit fullscreen mode

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' 
});

Enter fullscreen mode Exit fullscreen mode

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');

Enter fullscreen mode Exit fullscreen mode

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!


This article was created with the help of AI

Top comments (9)

Collapse
 
nazar-boyko profile image
Nazar Boyko

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.

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗡𝗻

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 fetch call 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 (like cookies(), headers(), or searchParams), Next.js looks at it during next build and 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 newer dynamicIO flags).

Seriously good call-out. It is a massive footgun that definitely trips people up after the flip.

Collapse
 
nazar-boyko profile image
Nazar Boyko

Thanks for sharing!

Thread Thread
 
johnnylemonny profile image
𝗝𝗼𝗡𝗻

You're welcome!

Collapse
 
frank_signorini profile image
Frank

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.

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗡𝗻

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.

Collapse
 
raju_dandigam profile image
Raju Dandigam

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.

Collapse
 
johnnylemonny profile image
𝗝𝗼𝗡𝗻

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-store across 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 revalidateTag strategies 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.

Collapse
 
nenoke profile image
Kyle007

Uncached by default feels much more intuitive. Fetch fresh data first, then add caching only when needed.