Next.js 15 App Router Caching(Part 2): Data cache

Learn how to cache requests efficiently in Next.js 15 App Router using built-in tools like fetch cache and revalidation.

Published
Updated

Next.js 15 App Router offers several layers of caching:

  1. Request Memoization
  2. Data Cache(current post)
  3. Full Route Cache
  4. Router Cache
  5. React Cache

Let's dive into the second: Data Cache


What is Data Cache?

In the Next.js 15 App Router, the data cache is a mechanism for caching fetch requests. Unlike request memoization, which stores results in RAM during a single request, the data cache stores results on disk in the .next/cache folder and persists across many requests.


How it works?

Next.js extends the native fetch API on the server. You can control caching by passing options to fetch

  • cache
  • next.revalidate
  • next.tags


Example of cached fetch

app/page.tsx
export const dynamic = 'force-dynamic'// Force page to be SSR for the example.

async function HomePage() {
  const response = await fetch('https://kolarclub.com/api/posts', {
    cache: 'force-cache', // Request will be cached
    next: {
      revalidate: 120, // Optional. Can be used for automatic revalidation. Given in seconds
      tags: ['posts'], // Optional. Can be used to revalidate with revalidateTag('posts')
    },
  });

  return null;
}

Let’s say we have two browsers (A and B), and the above SSR HomePage:

  1. Browser A opens https://kolarclub.com/: the fetch runs and hits the API.
  2. Browser B visits the page within 120 seconds: fetch returns from the cache — no API call.
  3. After 120 seconds, the cache expires and the process starts again.


Where Data Cache works?

  • Server Components
  • Route Handlers
  • Server Actions


How long is it cached?

Until it is revalidated. The cache can persist across:

  • Multiple requests
  • Multiple deployments


How can be revalidated?

Clearing of the data cache is called "revalidation". The revalidation depends on how it is configured

  • Automatic revalidation

Use next: { revalidate: N } — revalidates every N seconds.

  • Manual revalidation

Use next: { revalidate: false } and revalidate with revalidatePath and revalidateTag

Important

In both cases cache can be revalidated manually with revalidatePath and revalidateTag


Lets explain cache, revalidate and tags

With this 3 options passed to fetch you can configure caching of the request.

Pseudo typescript
fetch('...', {
  cache:'no-store' // 'force-cache' | 'no-store'
  next:{
    revalidate: undefined // false | number | undefined
    tags: undefined // string[] | undefined
  }
})


Option cache

  • 'force-cache' – cache the response
  • 'no-store' – do not cache anything
Important

Default in Next.js 15 is 'no-store', meaning fetches are not cached unless explicitly set.


Option revalidate

  • number – cache response and revalidate after X seconds
  • false – do not revalidate automatically, only via revalidateTag() or revalidatePath()
  • 0 – do not cache at all (acts like 'no-store')
Important

Default is undefined, which behaves like false. Use false explicitly to make this behavior clear.


Option tags

It is array of strings. Used to have fine-grained control over caching behaviour.

  • if you call revalidatePath all fetch requests on the page will be revalidated.
  • if you call revalidateTag you choose the exact fetch requests

Examples

Here it is few examples that I wondered about.

SSR page with cached fetch (revalidates on demand)

tsx
export const dynamic = 'force-dynamic';

export default async function Page() {
  const res = await fetch('https://example.com/api/items', {
    cache: 'force-cache',
    next: {
      revalidate: false,
      tags: ['items'],
    },
  });

  return null;
}


Static page with cached fetch (can still revalidate manually)

tsx
export const dynamic = 'force-static';

export default async function Page() {
  const res = await fetch('https://example.com/api/items', {
    cache: 'force-cache',
    next: {
      revalidate: false,
      tags: ['items'],
    },
  });

  return null;
}
Important

Both revalidatePath() and revalidateTag() will revalidate the page and fetch because the page is generated at build time.


SSR page with no cache

tsx
export const dynamic = 'force-dynamic';

export default async function Page() {
  const res = await fetch('https://example.com/api/items', {
    cache: 'no-store',
    next: {
      revalidate: false, // This is useless
    },
  });

  return null;
}

Where Is the cache stored?

  • Locally: in .next/cache after running next build
  • On Vercel: in the Edge Network for fast global access

How to test it locally?

In development mode (next dev), caching behaves inconsistently.
You should run: next build && next start

For myself I made helper script: "bs": "next build && next start"


How I understood every aspect of Next.js 15 App Router?

I created empty project where I played and tested the caching behaviour. Here are my files

app/page.tsx
import RevalideteButton from './components/RevalidateButton';

export const dynamic = 'force-dynamic';

export default async function Page() {
  const res = await fetch('http://localhost:3000/api', {
    cache: 'force-cache',
    next: {
      revalidate: 200,
      tags: ['test'],
    },
  });

  const data = await res.json();

  console.log('---Page: Response received!', data);

  return (
    <div className="flex flex-col max-w-md">
      <div>{data}</div>
      <RevalideteButton path="/dev" label="Revalidate Path" />
      <RevalideteButton tag="test" label="Revalidate Tag" />
    </div>
  );
}
app/components/RevalidateButton.tsx
'use client';

import { revalidate } from '../actions/revalidate';

export default function RevalidateButton({ tag, path, label }) {
  return (
    <button
      onClick={() => revalidate(tag, path)}
      className="btn btn-ghost"
    >
      {label}
    </button>
  );
}
app/actions/revalidate.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function revalidate(tag?: string, path?: string) {
  if (tag) revalidateTag(tag);
  if (path) revalidatePath(path);
  if (!tag && !path) throw new Error('Nothing to revalidate');
}
app/api/route.ts
export async function GET() {
    console.log('---Route: Request handled!')

    return Response.json('Hello, world!', { status: 200 })
}

On first request you’ll see:

---Route: Request handled!
---Page: Response received!

On cached fetches (after first request), only the page log appears:

---Page: Response received!


One more thing

If you dont have access directly to the fetch function, for example using some client like await cms.getPosts(), you still can cache the request same way but using unstable_cache from next. This will be explored more on the next posts here in the blog.

Final Tips

Understanding of data cache in Next.js App Router requires you to play with it so take the most of the examples and write them on your project!