← All articles

[ Blog ]

September 11, 2026

12 min read

Category: Insights

Vercel Deploy Not Updating Content (Cache, Not Code)

Vercel deploy not updating content? The build shipped fine — the cache outlived it. How to prove it in 30 seconds and the fix your developer owed you.

Next.jsVercelCachingCMSPerformance
Vercel Deploy Not Updating Content (Cache, Not Code)

You fixed the pricing on your homepage. You hit publish in the CMS, or your developer pushed the change. The deployment dashboard is green. You refresh the live site — old price. You refresh in incognito — old price. You send it to a colleague in another country — old price. Twenty minutes later, a client emails asking why the page says something you corrected this morning.

If your Vercel deploy is not updating content, the instinct everyone has is "the deploy didn't run, redeploy it." That instinct is wrong, and chasing it is what costs the afternoon. On a modern Next.js site the code and the content ship on two completely separate tracks. A deployment moves the code. It does not, by design, move the content. This post shows you how to prove which one is actually stale, how to fix it in about 60 seconds, and why the fact that you had to find this out yourself is a build decision someone made for you.

What this actually costs you

Nobody files a support ticket for "cache semantics." They file it for:

  • A launch where the price, date, or legal copy on the live page is wrong for hours after it was corrected.
  • A paid campaign driving traffic to a landing page that still shows last quarter's offer.
  • A client or investor who sees the old version and now quietly wonders what else on the site is wrong.
  • A content team that stops trusting the CMS and starts asking a developer to "push it live," which is exactly the bottleneck the CMS was bought to remove.

The technical bug is small. The trust damage is the expensive part, because the site is now, from the owner's point of view, lying about its own content.

Step 1: prove the new code is live (30 seconds)

In plain terms: before you touch anything, find out whether the problem is the code or the content. They fail differently and the fixes are opposites. Everyone skips this and redeploys three more times.

You cannot check this by looking at file names. Vercel content-hashes its JavaScript bundles, so an identical rebuild produces identical file names — an empty commit and a redeploy will look like "nothing changed" even when the deploy worked perfectly. Instead, grep the served JavaScript for a string that only exists in the new build. A new button label, a new component name, a new copy string — anything the previous version did not contain:

curl -s https://example.com/ \
  | grep -o '/_next/static/[^" ]*\.js' | sort -u \
  | while read -r p; do curl -s "https://example.com$p"; done \
  | grep -c "Book a discovery call"

A count above 0 means the new build is live on the CDN right now. The deployment did its job. Stop redeploying — you are about to fix the wrong thing.

Then check the content the same way, straight out of the HTML the server sent, with no browser cache involved:

curl -s https://example.com/pricing | grep -o "€[0-9,.]*" | head

New code + old content is the signature of this whole bug class. Your build is fine; your data is frozen.

Step 2: why a green deploy does not refresh content

Here is the part that surprises almost everyone, including experienced developers the first time it bites them.

In plain terms: the store of content your site renders from is a separate system that survives deployments on purpose. Shipping new code does not empty it.

Vercel's own documentation on the Data Cache states it plainly: cached data is "persistent across deployments" and "Cache is not updated at build time." Next.js says the same about unstable_cache — it "uses Next.js' built-in cache to persist the result across requests and deployments."

That is not a bug. It is the feature that keeps your site fast and your database bill low. A deployment does not stampede every page back to your CMS or your Postgres instance. (The flip side of getting this wrong is a site that hammers the CMS on every single request — that's the failure mode in why Next.js keeps hitting your CMS on every request, and it shows up on the invoice.)

The trap is the combination of that persistence with a cache entry that has no expiry. A typical CMS-backed page is wired like this:

import { unstable_cache } from 'next/cache';

export const getPage = unstable_cache(
  async (slug: string) => db.page.findUnique({ where: { slug } }),
  ['cms-page'],
  { tags: ['cms:pages'], revalidate: false },
);

In plain terms: revalidate: false means "keep this forever." The Next.js docs are explicit — omit revalidate or pass false and the data is cached indefinitely "until matching revalidateTag() or revalidatePath() methods are called." No timer. No expiry. Deploying ten more times changes nothing, because the new build reads the same surviving cache entry the old build wrote.

So there are two independent levers, and the dashboard only has one of them:

ActionShips new codeShips new content
git push → Vercel buildYesNo
Redeploy buttonYesNo
Clearing your browser cacheNoNo
revalidateTag()NoYes
Manual Data Cache purge in the dashboardNoYes (blunt — see below)

Read that table once and the whole 20-minute wild goose chase makes sense. Nothing in a normal deploy flow touches the content axis.

Step 3: the fix, in about 60 seconds

In plain terms: you need one authenticated URL you can hit that tells the site "this content changed, go read it again." On a well-built site it already exists and your publish button calls it automatically.

The endpoint is a small route handler:

import { revalidateTag } from 'next/cache';
import { NextResponse, type NextRequest } from 'next/server';

const TAGS = ['cms:pages', 'cms:collection:blog_posts'];

export async function POST(req: NextRequest) {
  const secret = process.env.REVALIDATE_SECRET;
  if (!secret || req.headers.get('x-revalidate-secret') !== secret) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  for (const tag of TAGS) revalidateTag(tag, 'max');

  return NextResponse.json({ revalidated: true, tags: TAGS });
}

Note the second argument. As of Next.js 16 the revalidateTag signature is revalidateTag(tag, profile), and the single-argument form is deprecated. 'max' is the recommended profile: visitors get served the existing page instantly while the fresh version is fetched behind them, so nobody waits. If you need the stale copy gone immediately — a wrong price, a legal correction — pass { expire: 0 } instead and accept one slow request.

Then the emergency button, the one to keep in a note somewhere:

curl -X POST https://example.com/api/revalidate \
  -H "x-revalidate-secret: $REVALIDATE_SECRET"
# {"revalidated":true,"tags":["cms:pages","cms:collection:blog_posts"]}

Vercel propagates that invalidation to every region within about 300ms. Content is correct on the next request, worldwide, with no build, no downtime, and no deploy queue. Compare that with the redeploy you were about to trigger: two to four minutes of build time, and it would not have fixed anything.

There is also a manual purge in the Vercel dashboard (CDN → Caches → Purge). Treat it as a fire axe, not a tool. On Hobby and Pro plans every project in the team shares one Data Cache, so purging it empties the cache for all of your sites at once and every page gets slow for a while as they refetch.

Watching this happen on a site you own right now? If nobody on your team can point at the revalidation endpoint, it probably doesn't exist — and that's fixable in an afternoon. Tell me what your stack is and I'll tell you exactly where the invalidation path is missing.

The same bug wearing a different costume

This is a class of failure, not one vendor's quirk. The signature is always: the data source has the new value, the rendered page does not, and there is no error anywhere.

I hit the local-development version of it on a Sanity-backed build. next dev server-rendered an empty taxonomy list while querying the Sanity API directly — same project, same dataset, same environment variables — returned the new documents immediately. No error in the terminal, because the fetch helper swallowed it. Deleting .next/cache/fetch-cache did nothing.

Two things were going on. First, the Turbopack dev server keeps its fetch cache in .next/dev/cache/fetch-cache, not the .next/cache/ path everyone reaches for from production habit. Second, the live-content invalidation depended on a browser subscription that was silently dead: the local port wasn't in the CMS's allowed CORS origins, so the subscription never connected and the cache was never told anything had changed. Data frozen at the first fetch, forever.

# the dev-server version of "why is this stale"
rm -rf .next/dev/cache/fetch-cache && yarn dev

Different host, different CMS, different cache directory — identical shape. And the diagnostic is identical too: compare what the server rendered against what the data source returns, right now, from the same environment. When they disagree, it is a cache, and only an invalidation call will move it.

A close relative worth knowing about: if the fetch fails once while the cache is being populated and the code quietly returns null, that null gets cached forever under exactly the same rules. The page then shows empty state permanently and no redeploy fixes it either — I wrote that one up in Next.js unstable_cache caching null forever.

This is a build decision, not a content-team mistake

Here is the part worth saying to whoever is reading this because the site they paid for is misbehaving.

Nobody on your marketing team did anything wrong. You published content. The CMS saved it. The website chose not to look. The persistence behaviour is a deliberate architectural trade — it is what makes a CMS-backed site fast and cheap to run — and taking that trade obliges whoever built the site to wire up the other half: an invalidation path that fires automatically when content changes.

On a properly wired build, that half is invisible. Publish in the CMS, a webhook or a server action calls revalidateTag with the tags that page depends on, the live page is correct within a second. Nobody types a curl command. Nobody redeploys. The content team never learns the word "cache."

When it is missing, the tell is the workaround everyone develops: "if the site looks stale, ask a developer to redeploy." That sentence is a bug report about the build. It means content edits are silently gated behind an engineer's availability, and it means your site is periodically wrong in public for as long as it takes someone to notice. The specific questions to ask a developer about this — before you sign anything — are in questions to ask a web developer before hiring.

Caching is one of those areas where the difference between a site that was assembled and a site that was engineered only becomes visible months after launch, usually at the worst moment. So is the bill: the same misunderstanding in the other direction is why a low-traffic site can blow through a serverless database limit.

The checklist to hand your developer

Copy this into an email. Every answer should be a specific file, URL, or endpoint — not "yes."

  1. Which pages are cached with no expiry? Anything using revalidate: false, or 'use cache' without a cacheLife, never refreshes on its own.
  2. What invalidates them? A named endpoint, server action, or webhook — and who calls it.
  3. Does the CMS publish button call it? If publishing content requires a human to also do something else, it will eventually be forgotten.
  4. Is there a manual override? One documented command a non-developer can run when something is visibly wrong at 9pm.
  5. Are cache tags granular? Invalidating one blog post should not force every page on the site to refetch.
  6. What happens if the fetch fails during revalidation? If the answer is "it caches the error," you have the permanent-empty-state bug waiting.
  7. Next.js 16 specifically: is this still on unstable_cache, or migrated to use cache / Cache Components? The unstable_cache docs now mark it as replaced. Both work; you should know which one you have, because the invalidation APIs differ.

FAQ

Why does my website still show old content after an update?

Almost always a cache between your CMS and the visitor, and on a Next.js/Vercel site it is usually the Data Cache, which persists across deployments by design. Check the served HTML with curl rather than a browser — if curl shows the old content too, it is a server-side cache, not your browser, and hard-refreshing will never fix it.

I redeployed and the content is still old. Why?

Because deploying and revalidating are independent. A deployment ships code; cached data survives it and the new build reads the same stale entries. Vercel's documentation says the cache is not updated at build time. Call revalidateTag instead.

How long does revalidateTag take to work?

Vercel propagates the invalidation to all regions within roughly 300ms. The refetch itself is triggered by the next request to an affected page, not by the call, so pages refresh as visitors hit them rather than all at once.

Is clearing the browser cache going to help?

Only if the stale copy is in your browser — test in incognito or with curl. If curl returns the old content, every visitor in the world sees the old content, and no amount of clearing browser caches on your side changes that.

Should I just set a short revalidate time on everything?

It is a workaround, not a fix. A 60-second revalidate means content is wrong for up to a minute, and your CMS or database gets hit far more often — which is exactly how hosting bills quietly grow. Tag-based on-demand revalidation gives you correct content immediately and a cache that actually caches.

Does this apply outside Vercel and Next.js?

The specific APIs, no. The failure shape, absolutely. Any setup with a persistent data cache and a broken or missing invalidation path produces the same symptom: the CMS has the new content, the site doesn't, and no error is logged anywhere. The dev-server case above was a completely different host and CMS with an identical signature.


Shipping something where wrong content in public is expensive? I build Next.js sites where the content path is engineered as carefully as the animation — tagged caches, automatic invalidation on publish, and a documented override for the day something goes sideways. Have a look at recent work, then tell me about the project.

Related posts