KolarclubPersonal website

Next.js 15 App Router Caching, Part 2: Data Cache

How to persist server-side fetch results across requests with force-cache, time-based revalidation, tags, and paths in Next.js 15.

This article was originally published on the previous Kolarclub website on 30 April 2025. It was transferred here on 22 July 2026 and revised for clarity and technical accuracy.

Next.js 15 App Router has several caching layers:

  1. Request Memoization
  2. Data Cache
  3. Full Route Cache
  4. Router Cache
  5. React cache

This part focuses on the Data Cache: the persistent server-side cache for data returned by fetch.

Data Cache vs. Request Memoization

Request memoization and the Data Cache can both reuse a fetch result, but their lifetimes are different.

Mechanism Scope Typical lifetime
Request Memoization One React server render Until that render finishes
Data Cache Shared server-side cache Across incoming requests and, depending on the platform, deployments

Request memoization happens automatically for matching GET and HEAD requests during rendering. The Data Cache is something you explicitly opt into in Next.js 15.

Opting a Request into the Data Cache

In Next.js 15, a server-side fetch without an explicit caching option behaves like cache: 'no-store'. Use force-cache when you want the response stored:

export default async function Page() {
  const response = await fetch('https://example.com/api/posts', {
    cache: 'force-cache',
  })

  const posts = await response.json()

  return <PostList posts={posts} />
}

On a cache miss, Next.js requests the data and stores the response. On later requests, a matching fetch can read that response from the Data Cache.

The physical storage is an implementation and hosting detail. Treat the Data Cache as a Next.js abstraction instead of relying on a specific local directory or storage product.

Time-Based Revalidation

Use next.revalidate to allow a cached result to become stale after a number of seconds:

const response = await fetch('https://example.com/api/posts', {
  next: { revalidate: 120 },
})

This means the response can be reused for 120 seconds. After that period, the next request may receive the stale value while Next.js refreshes it in the background. If the refresh succeeds, later requests receive the new value.

next.revalidate accepts:

  • A positive number — cache and revalidate after that many seconds.
  • false — cache indefinitely until an on-demand invalidation or deployment behavior removes it.
  • 0 — do not use the Data Cache for this request.

Do not combine contradictory options such as cache: 'no-store' and a positive revalidate value.

Tag-Based Revalidation

Tags let you invalidate a related group of cached requests:

const response = await fetch('https://example.com/api/posts', {
  cache: 'force-cache',
  next: {
    tags: ['posts'],
  },
})

When the content changes, a Server Action or Route Handler can invalidate the tag:

'use server'

import { revalidateTag } from 'next/cache'

export async function refreshPosts() {
  revalidateTag('posts')
}

Use a tag when several pages or components depend on the same logical data set.

Path-Based Revalidation

Use revalidatePath when the route is the natural unit you want to refresh:

'use server'

import { revalidatePath } from 'next/cache'

export async function refreshBlog() {
  revalidatePath('/blog')
}

A simple rule of thumb:

  • Use revalidateTag for shared data such as posts, products, or user:123.
  • Use revalidatePath when you specifically need to refresh one page or route subtree.

Cached Data on a Dynamically Rendered Route

The Data Cache and the Full Route Cache are independent. A route can render for every incoming request while still reusing selected data:

export const dynamic = 'force-dynamic'

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

  const catalog = await response.json()

  return <Catalog items={catalog} />
}

The page is rendered on every request, but the catalog request can still come from the Data Cache.

Uncached Data

Use no-store for request-specific or frequently changing data:

const response = await fetch('https://example.com/api/current-status', {
  cache: 'no-store',
})

The response is fetched again for each incoming render. Matching calls can still be memoized within that one render.

Testing Cache Behavior

Development mode adds conveniences for hot reloads and can reuse fetched data in ways that make cache testing confusing. For reliable testing, use a production build:

npm run build
npm run start

Add a log at the actual data source or inspect its request count. A log in the page component only proves that the page rendered; it does not prove whether fetch hit the Data Cache.

Practical Checklist

Before caching a request, answer four questions:

  1. Can different visitors safely share this response?
  2. How stale may the data become?
  3. Should an event invalidate it immediately?
  4. Is a tag or a route path the better invalidation boundary?

Those answers usually lead directly to no-store, force-cache, time-based revalidation, or on-demand revalidation.

For the precise version-specific behavior, see the Next.js 15 caching guide and the Next.js 15 fetch reference.