DEV Community

Cover image for Your Next.js Page Is Not Updating: Read the x-nextjs-cache Header Before You Debug Anything
Kausar Raza
Kausar Raza

Posted on Originally published at knowledgemarkg.com

Your Next.js Page Is Not Updating: Read the x-nextjs-cache Header Before You Debug Anything

You shipped a change. The database has it, the build succeeded, the deploy finished. You load
the page and it shows the old content.

At this point most people start debugging the thing they just changed. Nine times out of ten
on a Next.js site, nothing is broken. Before you touch anything, send one request and read one
header:

curl -s -D - -o /dev/null https://example.com/the-page | grep -i x-nextjs-cache
Enter fullscreen mode Exit fullscreen mode

That header separates "my deploy failed" from "the page is simply stale", and those are
completely different problems. I have confused them three times in two days on this site, and
twice I reached a wrong conclusion and had to walk it back.

What the values mean

x-nextjs-cache reports how the response you just received was produced:

  • HIT — served from the cache, and the cached entry is still considered fresh. What you see is what the cache holds.
  • MISS — nothing in the cache for this path, so it was rendered now. The content is current.
  • STALE — this is the one that fools you. The cached entry has passed its revalidation window, so the server handed you the old body and kicked off a regeneration in the background. Your next request will almost certainly be fresh.

Read that third one again, because it is the whole article. A STALE response is
not a failure. It is the cache doing exactly what it was configured to do: never make a visitor
wait for a rebuild. The cost is that the first person to ask for an expired page gets yesterday's
copy.

The trap, with the actual numbers

I had just fixed a metadata bug and deployed it. To verify, I crawled every URL in the sitemap
and checked whether the new tag was present. Result: 175 of 177 pages correct, two pages missing
the tag.

Two pages. That looks exactly like a real bug — some code path the fix missed. I was already
forming a theory about which one.

Then I requested one of those two pages again and read the headers:

x-nextjs-cache: STALE
Enter fullscreen mode Exit fullscreen mode

The page had not missed the fix. It was serving a copy generated before the deploy,
while regenerating behind the scenes. The very next request returned the correct tag. There was
no bug. There had never been a bug.

It gets worse, in an interesting way. Earlier the same day I ran two tools over the site back
to back — a crawler, then a content scanner. The crawler reported dead links that the scanner,
running two minutes later, could not find. Both were correct. The crawler's own requests
were what expired the cache and triggered regeneration
, so by the time the second tool
ran it was reading freshly generated pages. The first tool measured the old world; the second
measured the world the first tool had created.

If you take one habit from this: when a verification result surprises you, request the
page a second time before you believe it.

What actually controls this

For a route segment, the revalidation window is an export:

// app/blog/[slug]/page.tsx
export const revalidate = 3600;   // seconds
Enter fullscreen mode Exit fullscreen mode

That means: serve from cache for an hour; after the hour, the next request gets the stale copy
and triggers a rebuild. It does not mean "rebuild every hour" — nothing
regenerates if nobody asks. A page nobody visits stays stale indefinitely, which is why a
low-traffic site can show week-old content and a busy one feels instant.

Two values are worth knowing:

  • export const revalidate = 0 — never serve from the cache; render per request.
  • export const dynamic = "force-dynamic" — the same effect, stated as intent.

A sitemap route handler with revalidate = 3600 is a good illustration. Publish a
post and it will not appear in sitemap.xml immediately. It appears within the hour,
with no deploy and no manual step. I watched this happen yesterday: the post went live, the
sitemap still listed 61 URLs, and an hour later it listed 62. Nothing was broken in between.

When a timer is the wrong tool

Time-based revalidation assumes the content changes on its own schedule. It is the wrong tool
when you change something and need it live now — particularly when the change was made
outside the application.

That is the case people hit most often and understand least. If you update rows with a SQL
script, or edit content in a CMS that your app does not know about, the app has no idea anything
happened. No amount of redeploying helps, because the deploy is not what is stale — the cache
entry is.

For that, invalidate explicitly:

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

// after a write that the app performed
revalidatePath("/blog/the-slug");
revalidateTag("posts");
Enter fullscreen mode Exit fullscreen mode

Two things catch people here. revalidatePath only runs on the server — in a route
handler or a server action — so calling it from a client component does nothing. And it marks the
entry stale rather than rebuilding immediately, so the very next request may still return the old
body with x-nextjs-cache: STALE. That is not a failure either.

What a deploy does and does not clear

This depends on your host more than on Next.js, which is why the answers you find online
contradict each other.

On a platform that builds fresh and discards the old filesystem, a deploy effectively clears
everything. On a self-hosted setup where you extract a build over the previous one, whether the
on-disk cache survives depends on whether you deleted it. If your deploy copies files over the
top without removing .next/cache, entries can outlive the deploy that was supposed
to replace them.

Worth separating from a related failure that looks identical: on Windows, a running process
holds locks on files, so an extract over a live app silently skips them and reports success
anyway. That produces a genuinely half-updated app, not a stale cache — and the header will say
HIT or MISS rather than STALE. I covered that one, and the
deploy order that avoids it, in
the Next.js on IIS and Plesk write-up.

Three caches, and only one of them is this header

A lot of the contradictory advice online comes from people solving a different layer and
assuming it was the same one. There are usually three between your render and the visitor's
screen:

  • The Next.js data and route cache — on your server, reported by x-nextjs-cache. This is the one revalidate and revalidatePath control.
  • A CDN or reverse proxy in front of it, obeying cache-control. It can keep serving an old body long after Next.js has regenerated, and x-nextjs-cache will never tell you, because the response may not have reached Next.js at all.
  • The browser. Cheap to rule out, so rule it out first.

Two more headers help you tell them apart. age is how many seconds an
intermediate cache has been holding the response — a large and growing age on a page
you keep requesting means something in front of your server is answering. And
x-nextjs-prerender tells you the page was prerendered rather than rendered per
request, which is useful when you expected a route to be dynamic and it is not.

The practical ordering: hard-refresh once to eliminate the browser, then compare
curl against what the browser shows. If curl is correct and the browser is not, it
is local. If both are wrong but x-nextjs-cache says MISS, you are
probably looking at something in front of Next.js, not Next.js itself.

Reading it without curl

In Chrome or Firefox DevTools, open the Network tab, tick Disable cache, reload, click
the document request — the very first one, not an asset — and read Response Headers. The value is
there on every request Next.js serves, so you can watch it flip from STALE to
HIT on a second reload without leaving the browser.

One caveat that wastes time: if you are looking at a client-side navigation rather than a full
page load, there is no document request to inspect. Reload the URL directly.

A checklist for "is my change actually live?"

  • Request the page twice. If the second response differs from the first, you were looking at a stale entry, not a bug.
  • Read x-nextjs-cache. STALE means wait and retry; HIT or MISS with wrong content means something real is wrong.
  • Check the content at the source. Hit your API or query the database directly. If the source is wrong, the cache is innocent.
  • Decide which layer is stale. Content changed outside the app needs revalidatePath, not a redeploy. Code changes need a deploy, and a timer will never fix them.
  • Only then start debugging.

Compiled things — components, exported metadata, redirects in
next.config, anything with a NEXT_PUBLIC_ prefix — always need a
rebuild, because they are baked in at build time. That last one catches people constantly; I
wrote about a related environment-variable trap in
how .env.local silently overrides
.env.production
. If you are newer to the framework, the
complete Next.js
tutorial
covers the rendering model this builds on.

The short version

A page showing old content is usually not a broken deploy. It is a cache entry doing its job,
and one header tells you which. Checking it costs one request. Not checking it cost me two wrong
conclusions in two days, and both times I was already three steps into debugging code that was
working perfectly.

FAQ

What does x-nextjs-cache: STALE mean?

The cached copy has passed its revalidation window, so Next.js served you the old body and started regenerating the page in the background. It is not an error. Request the page again and you will almost certainly get the fresh version.

What is the difference between HIT, MISS and STALE?

HIT means it came from the cache and is still considered fresh. MISS means there was nothing cached so it was rendered now, which makes it current. STALE means the entry expired, you received the old body, and a rebuild was triggered behind your request.

Why is my Next.js page not updating after deploy?

Request it twice and read x-nextjs-cache. If the first response is STALE and the second is correct, nothing was wrong. If it says HIT or MISS and the content is still wrong, then check the source data and whether the deploy actually replaced the files.

Does export const revalidate = 3600 rebuild the page every hour?

No. It means the cached copy is considered fresh for an hour. Nothing regenerates unless someone requests the page after that window, so a page nobody visits can stay stale indefinitely.

Why is revalidatePath not working?

Two usual reasons. It only runs server-side, so calling it from a client component does nothing. And it marks the entry stale rather than rebuilding immediately, so the next request can still return the old body with a STALE header before the fresh one appears.

I changed data directly in the database and the page still shows the old content.

The application never saw the change, so nothing invalidated the cache, and redeploying will not help because the deploy is not what is stale. Either call revalidatePath for the affected routes, or wait out the revalidate window on that route.

Does deploying clear the Next.js cache?

It depends on the host. A platform that builds fresh and discards the old filesystem effectively clears it. On a self-hosted setup where you extract a build over the previous one, cache entries can survive unless you delete .next/cache as part of the deploy.


Originally published on knowledgemarkg.com.

Top comments (0)