Back to blog
October 18, 2026•5 views

Next.js 15 Caching Demystified: A Practical Guide for Developers

Next.js 15ReactCachingWeb PerformanceFull-Stack Development
Next.js 15 Caching Demystified: A Practical Guide for Developers

Next.js 15 changed caching more than any release since the App Router landed. If you upgraded and suddenly your dashboard stopped updating, or your marketing page started hitting the database on every request, you are not imagining it. The defaults flipped, and a lot of teams shipped the upgrade without reading the release notes closely enough.

This Next.js 15 caching guide is the version I wish I had when I migrated several production apps. I work on web engineering and AI automation, and I have hit most of these edge cases in anger. Below is what actually changed, how the four cache layers behave now, and the patterns I keep reusing.

The Four Caches You Are Actually Dealing With

Before touching code, get the mental model straight. Next.js has always had multiple caches, and confusing them is the root of 90% of "why is my data stale" bug reports.

Cache Layer Where It Lives What It Stores Default in Next.js 15
Request Memoization Server, per render pass Duplicate fetch calls in one render On (unchanged)
Data Cache Server, persistent Results of fetch and unstable_cache Off (changed from 14)
Full Route Cache Server, persistent Rendered HTML and RSC payload for static routes On for static routes only
Client Router Cache Browser memory Prefetched and visited route segments Reduced staleTime (changed)

The headline: the Data Cache no longer caches fetch by default. In Next.js 14, a plain fetch inside a Server Component was cached by default (with cache: 'force-cache' semantics) unless you opted out. In 15, it is dynamic unless you opt in. That single change is why half the "my site got slower" and half the "my site got faster" reports exist.

Request memoization still works, so calling the same URL twice in one render does not double the network cost. That part is safe to rely on.

The New fetch Defaults and How to Opt In

Here is the behavior in plain terms. In Next.js 15:

// Dynamic. Runs on every request. No cache entry created.
const res = await fetch('https://api.example.com/products');

// Cached. Stored in the Data Cache, revalidated at most every 60s.
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 },
});

// Cached until you invalidate it manually (or the entry is evicted).
const res = await fetch('https://api.example.com/products', {
  cache: 'force-cache',
});

That is the whole opt-in story. cache: 'force-cache' and next: { revalidate: n } both create Data Cache entries. Anything else is dynamic.

My rule of thumb for teams: if the data changes on a schedule you can name (catalog, CMS content, pricing tables), use revalidate. If it changes on an event you can name (a product gets edited, an order ships), use tags. If it changes constantly and correctness matters more than latency, leave it dynamic and put a CDN or a Redis layer in front of the upstream API instead.

Why They Flipped the Default

The Next.js team's reasoning is sound. Caching by default meant developers were shipping stale data without realizing it, and the bugs were silent. Making dynamic the default means the failure mode is "slower but correct" instead of "fast but wrong." For a Next.js data cache that serves financial or inventory data, correct wins.

The cost is that static-ish pages now need explicit intent. That is a fair trade, but it does mean your upgrade needs an audit, not just a version bump.

Revalidation: Time-Based, Tag-Based, and On-Demand

Revalidation is where Next.js 15 caching stops being a config exercise and becomes architecture. There are three mechanisms and they compose.

Time-Based Revalidation

revalidate: 60 on a fetch, or export const revalidate = 60 on a route segment. Simple, predictable, and good enough for most marketing content. The tradeoff is you either serve data up to 60 seconds old or you pay for more frequent regeneration.

Tag-Based Revalidation

Tags are the part most teams underuse. Tag your fetches, then invalidate by tag from a Server Action or Route Handler.

// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] },
  });
  return res.json();
}

// app/actions/update-product.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: FormData) {
  await db.products.update(id, data);
  revalidateTag('products');
}

When the product changes, every cached entry tagged products is invalidated. No polling, no guessing at a TTL. This is the pattern I reach for on any app with a CMS or admin panel, because editors expect their save to show up immediately.

If your mutations live in Server Actions, my practical tutorial on Next.js 15 Server Actions for form handling and data mutations walks through the full flow, including where to place revalidateTag so it fires reliably.

On-Demand Path Revalidation

revalidatePath('/products') still exists and is useful when a change affects a whole route. It is blunter than tags, so I use it sparingly. If you find yourself calling revalidatePath for the same route from five places, you probably want a tag instead.

unstable_cache and When to Use It

fetch is not the only thing you want to cache. Database queries, third-party SDK calls, and expensive computations do not go through fetch, so they need unstable_cache.

import { unstable_cache } from 'next/cache';

export const getTopCustomers = unstable_cache(
  async (region: string) => {
    return db.query.customers.findMany({ where: { region } });
  },
  ['top-customers'],
  { revalidate: 300, tags: ['customers'] }
);

Three things to know. The key parts array must include every argument that changes the result, or you will serve the wrong data. Tags work the same as with fetch, so revalidateTag('customers') clears it. And yes, it is still named unstable_cache, which is a fair warning: the API has shifted between versions, so pin your Next.js version and read the changelog before upgrading.

I have seen a production bug where a team cached a query keyed only on ['user'] while passing a userId argument. Every user got the first user's data. If you take one thing from this Next.js caching guide, take that: cache keys are a correctness concern, not a performance detail.

Route Handlers, Route Segment Config, and Client Router Cache

Route Handlers Are Not Cached by Default

In Next.js 14, GET Route Handlers were cached by default when they did not use dynamic APIs. In 15, they are dynamic by default. If you want caching, be explicit:

// app/api/rates/route.ts
export const dynamic = 'force-static';
export const revalidate = 3600;

export async function GET() {
  const rates = await getExchangeRates();
  return Response.json(rates);
}

This trips people up because a GET handler that used to be static now runs per request. If your API route hits a paid upstream, check this before you ship.

Route Segment Config Still Matters

export const dynamic, revalidate, and fetchCache on a page or layout still control the Full Route Cache. A page with no dynamic APIs and no dynamic fetches will be statically rendered and cached at build time. Add a cookies() call and it becomes dynamic. That interaction is the source of most "why is this page static" confusion.

Client Router Cache Got Tighter

The browser-side cache for prefetched pages now has a shorter stale time by default, and dynamic segments are not cached on the client in the same way they were in 14. Practically, this means back-button navigation may refetch more than it used to. That is usually the correct behavior for dashboards. For a content site, you may want to add staleTimes in next.config.js to restore some of the old snappiness:

// next.config.js
module.exports = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
};

On staleTimes status: it has historically been an experimental flag, and the Next.js team has been moving toward the cacheLife and use cache model (part of the newer caching direction) as the long-term replacement for hand-tuned stale times. Treat staleTimes as a transitional knob, verify whether it still lives under experimental in your exact Next.js 15 patch version by checking the current docs, and expect the use cache / cacheLife approach to become the recommended path for fine-grained cache control in future releases.

Tune this with real user data, not vibes. I have seen a 30-second dynamic stale time cut perceived navigation latency noticeably on list-heavy admin UIs.

Migrating From Next.js 14 Without Breaking Things

Here is the checklist I run on every upgrade. It takes an afternoon and saves a week of incident calls.

  1. Inventory every fetch call in Server Components and Route Handlers.
  2. For each one, decide: dynamic, time-based, or tag-based. Write the decision down.
  3. Add explicit next: { revalidate } or next: { tags } where you want caching.
  4. Remove redundant cache: 'no-store' calls (dynamic is now the default) to reduce noise.
  5. Audit unstable_cache keys for missing arguments.
  6. Check GET Route Handlers for accidental dynamic behavior.
  7. Test the back-button and prefetch experience on your slowest pages.
  8. Load test the endpoints that lost their implicit cache. This is the one people skip and regret.

That last point matters most. If you had a fetch that was silently cached and serving thousands of requests from one upstream call, making it dynamic can multiply your upstream traffic overnight. I have watched an API bill spike because of exactly this.

Caching Strategies by Rendering Mode

Different rendering modes want different Next.js caching strategies. This table is the one I keep pinned.

Rendering Mode Cache Config Best For Watch Out For
SSG No dynamic APIs, no dynamic fetch Marketing pages, docs, blogs Content changes need a rebuild or tag revalidation
ISR revalidate: n on fetch or segment Catalogs, listings, CMS pages Stale window is visible to users
SSR Dynamic fetch, cookies() or headers() Dashboards, auth-gated views Every request hits your data layer
PPR (experimental) Static shell plus dynamic holes Mixed pages with personalized sections Still evolving, test thoroughly

PPR is worth watching if you have pages that are 80% static with a personalized header. It lets you ship the static shell instantly and stream the dynamic parts. I would not bet a client's launch date on it yet, but I am testing it on internal tools.

Common Pitfalls and How to Debug Them

These are the ones I get asked about most, usually at 11pm before a launch.

Stale Data After an Edit

Almost always a missing tag or a revalidatePath pointed at the wrong route. Add a tag to the fetch, call revalidateTag in the mutation, and confirm the mutation actually runs on the server. Server Actions that throw silently are a classic cause.

Data Refetching on Every Request

The opposite problem. You removed a force-cache during the migration and never added revalidate. Go back to the inventory list and add explicit caching.

Different Results in Dev vs Prod

Dev mode does not cache the same way. Always verify caching behavior with next build && next start. I have lost hours to this and now treat dev as a debugging environment only, never as proof of caching behavior.

Cache Not Invalidating Across Instances

If you run multiple Node instances behind a load balancer, revalidateTag needs a shared cache handler. The default in-memory cache is per-instance. For multi-instance deployments, configure a custom cache handler backed by Redis or your platform's KV store. This is the single most common production caching bug I see on larger deployments.

A Real Error You Will Probably Hit

When you call revalidateTag or revalidatePath from a context that is not a Server Action or Route Handler, Next.js throws at build time:

Error: Route "/products" used `revalidateTag('products')`.
`revalidateTag` was called outside a Server Action or Route Handler.

The fix is to move the call inside a 'use server' function or a Route Handler, not a plain client component or a render-time code path. This error is easy to misread as a caching bug when it is really a placement bug.

A Short Note on Hiring for This

If you are bringing on help, verify that whoever you hire can explain the difference between the Data Cache and the Full Route Cache before you hand over production traffic.

FAQ

Q: Is fetch cached by default in Next.js 15?

A: No. In Next.js 15, fetch is dynamic by default. You opt into caching with cache: 'force-cache' or next: { revalidate: n }. This is the biggest behavioral change from Next.js 14, where fetch was cached by default.

Q: What is the difference between revalidatePath and revalidateTag?

A: revalidatePath invalidates everything cached for a specific route path. revalidateTag invalidates every cache entry that was tagged with a given tag, across any route. Tags are more precise and scale better, so I default to tags for content that changes from an admin panel.

Q: Do Route Handlers cache GET requests in Next.js 15?

A: Not by default. GET Route Handlers are dynamic unless you explicitly set export const dynamic = 'force-static' or add a revalidate value. If your handler hits a paid or rate-limited upstream, verify this before deploying.

Q: Why does my cached data not update in production with multiple servers?

A: The default cache is stored in memory per instance. With multiple instances behind a load balancer, revalidateTag only clears the instance that handled the request. Configure a shared cache handler backed by Redis or a KV store so all instances see the invalidation.

Q: Should I use unstable_cache for database queries?

A: Yes, if the query is expensive and the result can tolerate some staleness. Just make sure every argument that changes the result is part of the cache key, and tag it so you can invalidate it from your mutations.

Q: What is the use cache / cacheLife direction in Next.js?

A: The Next.js team is moving toward a use cache directive and cacheLife profiles as a more explicit, declarative way to control caching at the component and function level. This is intended to eventually reduce reliance on flags like staleTimes. Check the current Next.js 15 docs for the exact status in your version, since this area is still evolving.


The author works on web engineering and AI automation. This guide reflects hands-on experience migrating production Next.js apps from 14 to 15.

Frequently asked questions

Is fetch cached by default in Next.js 15?

No. In Next.js 15, fetch is dynamic by default. You opt into caching with cache: 'force-cache' or next: { revalidate: n }. This is the biggest behavioral change from Next.js 14, where fetch was cached by default.

What is the difference between revalidatePath and revalidateTag?

revalidatePath invalidates everything cached for a specific route path. revalidateTag invalidates every cache entry tagged with a given tag, across any route. Tags are more precise and scale better, so I default to tags for content that changes from an admin panel.

Do Route Handlers cache GET requests in Next.js 15?

Not by default. GET Route Handlers are dynamic unless you explicitly set export const dynamic = 'force-static' or add a revalidate value. If your handler hits a paid or rate-limited upstream, verify this before deploying.

Why does my cached data not update in production with multiple servers?

The default cache is stored in memory per instance. With multiple instances behind a load balancer, revalidateTag only clears the instance that handled the request. Configure a shared cache handler backed by Redis or a KV store so all instances see the invalidation.

Should I use unstable_cache for database queries?

Yes, if the query is expensive and the result can tolerate some staleness. Just make sure every argument that changes the result is part of the cache key, and tag it so you can invalidate it from your mutations.

What is the use cache / cacheLife direction in Next.js?

The Next.js team is moving toward a use cache directive and cacheLife profiles as a more explicit, declarative way to control caching at the component and function level. This is intended to eventually reduce reliance on flags like staleTimes. Check the current Next.js 15 docs for the exact status in your version, since this area is still evolving.

Share this post

Native Web Share is available on mobile — that share sheet includes Instagram and any other app you have installed.

Let's Connect

Get In Touch

Let's build something premium together.

Contact info

Follow me
Chat on WhatsApp