DEV Community

Anas Sheikh
Anas Sheikh

Posted on

You Upgraded to Next.js 15 and Your Site Got Slower. The Caching Defaults Flipped

You upgrade from Next.js 14 to 15. The app compiles, the tests pass, nothing visibly breaks. A week later your database bill is higher, your pages feel slower, and your Time to First Byte graph has a step in it that lines up exactly with the deploy.

Nothing is broken. Something that used to happen for free stopped happening.

What Changed

In Next.js 14, data fetching was cached by default. A fetch in a Server Component ran once, the result was stored, and later requests reused it. You had to opt out of caching.

In Next.js 15, the default flipped. Per the upgrade notes:

  • fetch requests are no longer cached by default.
  • GET route handlers are no longer cached by default.
  • The client-side router cache no longer holds page segments between navigations by default.

So code you wrote once and never touched now behaves differently:

// app/products/page.tsx
export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  return <ProductList products={products} />;
}
Enter fullscreen mode Exit fullscreen mode

On 14, this fetched the data once and served the page from cache. On 15, every visitor triggers the API call, and because the page now depends on data fetched at request time, the route is rendered dynamically.

Why It Is Easy to Miss

  • Nothing errors. The code is valid. It just does more work.
  • Dev mode hides it. In next dev, nearly everything is rendered per request anyway.
  • The slowdown is gradual. With low traffic, an extra API call per request is invisible. It shows up when the traffic does.
  • Third-party limits bite later. If the API you call has rate limits or per-request billing, you find out from the invoice.

How to See It in 30 Seconds

Run a production build and read the route table at the end:

next build
Enter fullscreen mode Exit fullscreen mode
Route (app)
┌ ○ /                 (Static)
├ ƒ /products         (Dynamic)
└ ƒ /blog/[slug]      (Dynamic)
Enter fullscreen mode Exit fullscreen mode

○ means the page is pre-rendered once at build time. ƒ means it runs on the server for every request. A page that used to be static and is now ƒ is exactly what this upgrade can cause. Compare the table before and after the upgrade.

Fix 1: Cache One Request on Purpose

const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});
Enter fullscreen mode Exit fullscreen mode

This restores the old behavior for that single request. Use it for data that does not change per user and rarely changes at all.

Fix 2: Cache With an Expiry

Most data is not permanent, just slow-changing. Revalidate it on a timer:

const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 }, // seconds
});
Enter fullscreen mode Exit fullscreen mode

Visitors get the cached copy, and after an hour the next request refreshes it in the background. This is the right default for product lists, blog posts and prices.

Fix 3: Set It Once for the Whole Page

If several fetches on a page should share the same freshness, declare it at the route level:

// app/products/page.tsx
export const revalidate = 3600;
Enter fullscreen mode Exit fullscreen mode

Everything that page renders is treated as cacheable for an hour, without repeating options on each call.

Fix 4: Route Handlers Need Their Own Opt-In

A GET route handler that returns the same data for everyone used to be cached automatically. Now you must say so:

// app/api/products/route.ts
export const dynamic = 'force-static';

export async function GET() {
  const products = await getProducts();
  return Response.json(products);
}
Enter fullscreen mode Exit fullscreen mode

Without that line, the handler runs on every request.

The Part People Forget: Your Database Was Never Covered

Here is the subtle thing. The fetch cache only ever applied to fetch. If you read from MongoDB or Postgres directly with an ORM, this change did not touch those calls. Depending on whether the page used request data such as cookies, it was either rendered once at build time with the data frozen in, or rendered on every request. Neither is controlled by the fetch cache, so check your build table for those pages too.

To cache a database call at runtime, wrap it:

import { unstable_cache } from 'next/cache';

const getProducts = unstable_cache(
  async () => Product.find().lean(),
  ['products'],
  { revalidate: 3600, tags: ['products'] }
);
Enter fullscreen mode Exit fullscreen mode

The tags option lets you clear that cache on demand with revalidateTag('products') after an edit. I wrote about why revalidateTag appears to do nothing when the data is not tagged in the first place. Newer Next.js versions keep moving toward explicit opt-in caching, so check the docs for the version you are on before copying any of this.

Should You Cache Everything Again?

No. The new default exists because stale data surprised people. Show a user their own cart, balance or notifications from a shared cache and you have a bug, or a data leak.

Data Choice
Public, rarely changes (marketing pages, docs) Cache it, long expiry
Public, changes often (product list, prices) Cache with a short revalidate
Per-user (dashboard, cart, account) Do not cache
Admin views Do not cache

The Rule

When a framework changes a default, your old code changes behavior without a single line changing. After any major upgrade, run next build before and after and compare which routes are static (○) and which are dynamic (ƒ). Then opt back in on purpose, for the data that deserves it.

Slow server responses hurt the metrics Google measures. I break those down in plain language in my guide to Core Web Vitals, and if you want a Next.js starting point, you can browse my templates on pixelanas.com.

Get the templates: https://pixelanas.gumroad.com

Did a framework upgrade ever quietly change your app's behavior? Tell me what it was in the comments.


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (0)