Knowledge Mark G
Hire Me

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

You deployed, the database says the change is there, and the live page still shows the old content. Before you start debugging, send one request and read a single header. It will tell you which of two completely different problems you have.

Kausar RazaBy Published on · 7 min read · 3 views
Share:

Web Development

Your Next.js Page Is Not Updating: Read the x-nextjs-cache Header Before You Debug Anything
  • Next.js 15 Tutorial
  • Performance
  • DevOps
  • Web Development

Need help with this?

I build and fix business websites - fast, mobile-first and built to bring enquiries.

Get in touch

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

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

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

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");

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?"

  1. Request the page twice. If the second response differs from the first, you were looking at a stale entry, not a bug.
  2. Read x-nextjs-cache. STALE means wait and retry; HIT or MISS with wrong content means something real is wrong.
  3. Check the content at the source. Hit your API or query the database directly. If the source is wrong, the cache is innocent.
  4. 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.
  5. 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.

Frequently asked questions

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.

Join the conversation

No comments yet — be the first to share what you think.

Leave a comment

Never published.

Optional.

Keep reading

Related articles

View all