Jamstack Deploy Succeeded but Users Still See Stale Content: A Diagnostic Guide
Deploy succeeded but users see old pages? Diagnose whether the CDN, browser, service worker, or build data is serving stale HTML, then apply the right header-level fix.
14 Aug 2026, 03:34 UTC

Your deploy log says success, the commit SHA matches, and yet visitors — sometimes only some of them, sometimes only in certain regions — still see the old page. Meanwhile the hashed JavaScript and CSS bundles look current. This is the classic Jamstack staleness problem, and it is almost never a single bug. It is a caching decision made somewhere in a chain of layers: the CDN edge, the browser, and possibly a service worker. The useful takeaway: find out which layer served the stale response before changing anything, because the fix for each layer is different and the wrong fix (like no-store on everything) destroys the performance benefit that made Jamstack attractive.
Recognize the condition
The signature pattern: HTML is stale but fingerprinted assets (files like app.a1b2c3.js) are fresh. Staleness is often intermittent — one colleague sees the new page, another doesn't; it works on your phone but not your laptop. That intermittency is your first clue that a shared cache (CDN edge node) or a per-client cache (browser, service worker) is involved, not the build itself.
Cause and diagnostic table
| Symptom | Likely cause | Confirming signal |
|---|---|---|
| Stale for many users, same region | CDN cached HTML with a long max-age | age header counting up; CDN cache-status shows HIT |
| Stale only on your machine | Browser cache | Disappears with cache disabled in devtools |
| Stale even in incognito with cache off | Service worker serving a cached app shell | DevTools Application tab shows an active worker |
| HTML fresh, data old | Build ran against stale CMS/API data | Deploy timestamp predates the content change; no webhook fired |
| Preview URL correct, production wrong | Change landed on a preview/branch deploy | Production deploy SHA doesn't match your commit |
| Fresh after a delay, then stale again | ISR / stale-while-revalidate window | Cache-status shows STALE or REVALIDATED |
Ordered checks
1. Confirm the production deploy actually contains the change
In your host's dashboard, compare the live production deploy's commit SHA and build time against your change. Also check the deploy's file listing: does the HTML reference the new asset hashes? If the change went to a branch or preview deploy, retarget the production branch or promote the deploy — no cache work is needed.
2. Inspect the live response headers
From any machine with network access to the site (no special permissions needed), run:
curl -sD - -o /dev/null https://your-site.example/pageRead cache-control, age, etag, and the CDN status field your provider documents — for example cf-cache-status on Cloudflare, x-vercel-cache on Vercel, x-cache on CloudFront. A HIT with a large age means the edge served a cached copy. A long max-age on the HTML tells you why. Replace the URL with the exact page users report as stale; cache behavior can differ per route.
3. Rule out the browser
Open devtools, enable "Disable cache" on the Network tab, and hard-reload. If the fresh page appears, the problem is your local cache policy — the fix belongs in response headers, not the CDN.
4. Check for a controlling service worker
In devtools, open the Application tab and look at Service Workers. If one is active, unregister it and hard-reload. If staleness disappears, the worker's cache is serving an old app shell. Note that service worker updates are asynchronous: users typically need a second visit before new code activates, and aggressive skipWaiting() can mix an old HTML shell with new assets (or vice versa), causing broken pages rather than stale ones.
5. Verify the data was fresh at build time
For statically generated pages, the HTML is only as fresh as the data fetched during the build. Check the deploy log's fetch timestamps against when the CMS content changed. If your CMS webhook didn't fire (or isn't configured), the build succeeded with old data — a build trigger problem, not a cache problem.
Fixes tied to findings
CDN or browser caching HTML too long
Adopt a split policy: mutable HTML gets revalidation, content-hashed assets get immutability. On Netlify, for example, via a _headers file:
/*.html
Cache-Control: public, max-age=0, must-revalidate
/assets/*
Cache-Control: public, max-age=31536000, immutableThe immutable directive is safe only because the filenames change when content changes. max-age=0, must-revalidate on HTML forces a conditional request; with a valid etag, the server answers 304 when nothing changed, so revalidation is cheap. Exact configuration syntax differs across Netlify, Vercel, Cloudflare Pages, and raw CDNs — verify against your provider's current documentation rather than assuming defaults.
Service worker serving stale shell
Bump the cache version in the worker so old caches are deleted on activate, and consider a versioned precache manifest so each deploy's worker only fetches its own hashed assets. Test the update flow explicitly: first visit installs, second visit activates.
Stale build data
Wire a CMS webhook to your host's build hook so content changes trigger rebuilds. Verify by making a trivial content edit and confirming a new deploy starts within a minute.
ISR / stale-while-revalidate windows
ISR semantics have changed across framework versions, so confirm behavior for the exact version pinned in your project. If your framework supports on-demand revalidation, trigger it from the same CMS webhook for pages that must update immediately, and keep time-based revalidation for the rest. Choose the staleness window per route: a marketing page tolerates minutes; a pricing page may not.
Avoid two tempting non-fixes. First, query-string busting (?v=2) is unreliable — some CDNs ignore or normalize query strings by default. Second, no-store on everything eliminates staleness but also eliminates the edge cache, raising origin load and latency for every visitor.
Escalation criteria
Contact platform support when: cache-status headers show persistent HITs after an explicit purge request; the purge API errors or isn't available on your plan; cache-disabled, service-worker-free clients still receive stale HTML; or the dashboard marks a deploy live while old bundles are served at the production URL. Include the deploy ID, request IDs from response headers, timestamps with timezone, and full header captures from your curl runs — support can trace an edge node's behavior only with those identifiers.
Verify the fix
Re-run the same curl -sD - command after each change and after a fresh deploy. You want to see HTML with max-age=0 (or a short TTL) plus an etag, and a MISS or revalidated response immediately after deploy, flipping to HIT afterward. Compare the asset hashes in the served HTML against the latest deploy's file listing — matching hashes across regions and browsers is the real success criterion.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.