Load, change, and cache data

Why Is My Next.js App Router Data Still Stale? Find the Cache Layer

Trace stale App Router data through Next.js caches and your CDN, then choose an uncached fetch, targeted revalidation, or a CDN purge.

Editorial illustration for Why Is My Next.js App Router Data Still Stale? Find the Cache Layer

If an App Router page still shows an old value, first ask which artifact is old. An uncached data request will not fix a CDN that keeps returning yesterday’s HTML. Likewise, purging the CDN will not change data that Next.js still serves from its server cache. Identify the cache layer, then change the control for that layer.

Check which App Router caching model you use

Look for cacheComponents: true in next.config.ts. That enables the Cache Components model. Without it, use the previous App Router caching model when interpreting fetch and revalidation behavior. The Cache Components caching guide explicitly separates the two; advice for one should not be applied to the other without checking.

This article is about the App Router, not Pages Router cache controls. That distinction matters most before copying a freshness fix into an existing project.

Name the artifact that is stale

Layer Artifact to inspect Relevant freshness decision
Data cache The result reused for a data request In the previous model, opt an individual fetch out of caching or revalidate cached data by path or tag.
Rendered output The cached result of rendering a route or, with Cache Components, a cached component Check whether the rendered result is reused even when the underlying source has changed.
CDN response cache An HTTP response, including its HTML or RSC variant Inspect response caching and purge the affected CDN entries when necessary.

These are not three names for the same cache. In the previous model, even a third-party data client that does not use fetch can have its rendered output reused when its route segment is static; a dynamic segment refetches that client’s data when rendered. In the Cache Components model, use cache can instead cache an async function’s return value or a whole component’s output. The previous-model fetching guide and Cache Components guide describe those different decisions.

In the previous model, choose an uncached fetch or revalidation

If this particular fetch must obtain data again on a subsequent request, opt it out of the Data Cache. This is a previous-model example; replace the URL with your data source:

export async function getInventory() {
  const response = await fetch('https://api.example.com/inventory', {
    cache: 'no-store',
  })

  if (!response.ok) throw new Error('Could not load inventory')
  return response.json()
}

The previous-model documentation also lists revalidate: 0 as an individual-fetch opt-out. Neither setting makes an unchanged upstream response newer, and neither purges a CDN copy of a page.

If caching is useful but the data changes after a write, invalidate the intended path or tag instead. For example, this Server Action can be called after the write that changes the inventory page’s data:

import { revalidatePath } from 'next/cache'

export async function refreshInventoryPage() {
  'use server'
  revalidatePath('/inventory')
}

Use revalidateTag when your cached data is organized by cache tag rather than by the affected path. Both on-demand methods can be called in a Server Action or Route Handler. Make the write succeed before treating its revalidation call as the freshness step; the fragment above performs no write itself.

With Cache Components, cache only work you intend to reuse

Under cacheComponents: true, use cache can go on an async data function or on a component. Here is a data-function fragment for information that may be reused:

export async function getInventory() {
  'use cache'
  // Pair this cached scope with a cacheLife chosen for your data.
  const response = await fetch('https://api.example.com/inventory')

  if (!response.ok) throw new Error('Could not load inventory')
  return response.json()
}

Set a cacheLife for the cached scope based on how long that result should remain useful. The Cache Components guide recommends pairing each cache directive with one; otherwise an implicit default profile applies. There is no universal inventory lifetime to copy into every application.

For async work that must be fresh on every request, leave use cache off that work and put its component behind Suspense. This is a separate Cache Components example, not a wrapper around the cached function above:

import { Suspense } from 'react'

async function LiveInventory() {
  const response = await fetch('https://api.example.com/inventory')
  if (!response.ok) throw new Error('Could not load inventory')

  const items: { id: string; name: string }[] = await response.json()
  return <ul>{items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>
}

export default function InventoryPage() {
  return (
    <Suspense fallback={<p>Loading inventory…</p>}>
      <LiveInventory />
    </Suspense>
  )
}

In this model, the Suspense fallback can ship in the prerendered shell while the uncached async work runs at request time. Moving use cache onto LiveInventory would change the freshness decision, not merely the loading UI.

Check revalidation, then inspect the CDN response

If a cached value remains old after revalidation, check whether revalidation actually completed. In the previous model, when data revalidation throws an error, Next.js continues serving the last successfully generated cached data and retries on a subsequent request. An old value is therefore not, by itself, proof that the invalidation call was never made.

Next, inspect the response headers. The following are CDN-facing Cache-Control behaviors documented for App Router routes, not settings for an individual Next.js data-cache entry:

Static, no revalidation: Cache-Control: s-maxage=31536000
ISR, time-based:      Cache-Control: s-maxage={revalidate}, stale-while-revalidate={expire - revalidate}
Dynamic, no caching:  Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate

The Next.js CDN caching guide explains the important split: revalidatePath() or revalidateTag() invalidates the Next.js server cache, but a CDN can keep serving its existing response until its s-maxage expires. If that response must change sooner, trigger a CDN purge alongside the Next.js revalidation for the affected keys, including HTML and RSC variants.

Finally, verify the CDN does not collapse those variants. Its cache key must retain the _rsc query parameter, and it must forward the rsc request header to the server. Stripping the parameter can select the wrong cached variant; stripping the header can make the server return HTML when client-side navigation expects an RSC payload. After a purge, check both the page response and a client-side navigation—not just the visible HTML—to confirm the intended variants refresh.

Find a note

Search by topic, title, or keyword.