Topic: Web platform
The site is deployed. Why am I still seeing the old version?
Follow a response through browser and CDN caches, understand s-maxage and validators, and connect fingerprinted JavaScript to the HTML that actually requests it.
Animated meme (expand/collapse)
A deployment finishes, but the page looks unchanged. Open DevTools, enable Disable cache, and it works. That result is a useful clue. It does not prove that ordinary visitors can get the new version.
I would start with the requested URLs and response headers. A cache may be following its configuration correctly, including permission to keep using an older response.
This article covers ordinary HTTP caching. Service Worker storage, framework data caches, and back-navigation snapshots need their own checks.
Who is keeping the response?
A browser HTTP cache serves an individual user. A shared cache, such as a CDN, can reuse a public response for different visitors. A JavaScript Map holding query results belongs to application code; calling it a cache does not make it follow HTTP policy.
Changing a file at the origin does not automatically notify every cache holding it. A fresh response may still be reused when policy permits. Stale does not mean the file definitely changed, either. Its freshness lifetime has expired.
MDN’s caching guide explains these categories. Identify the layer supplying a response before deciding why it considers that response reusable.
max-age and s-maxage apply to different caches
Consider a public product description that is identical for every visitor:
Cache-Control: max-age=120, s-maxage=900
Age: 180
| Cache | Applicable lifetime | At an age of 180 seconds |
|---|---|---|
| Browser | 120 seconds from max-age | Stale |
| Shared cache, such as a CDN | 900 seconds from s-maxage | Fresh |
Both values use seconds. Private caches ignore s-maxage. Shared caches use it in preference to max-age when present.
Age matters too. This response does not receive another fresh 120 seconds when it reaches the browser. Actual age calculations also involve Date and transit time; the example supplies the age directly to illustrate that passing through another layer does not reset it.
This policy can reduce origin traffic for public content. A short browser lifetime still does not guarantee immediate visibility of an origin change: the CDN may have a response it considers fresh. Immediate updates need a compatible purge or versioning strategy. See MDN Cache-Control for directive and age semantics.
A freshness lifetime is not a storage guarantee. A browser can evict an entry or a user can clear it.
no-cache permits storage
With no-cache, ordinary HTTP reuse requires successful validation. The response can still be stored. no-store asks HTTP caches not to store the response.
For article HTML that can receive corrections, this is a starting point to evaluate:
Cache-Control: no-cache
ETag: "article-v3"
For personalized responses that may be stored by a browser but not a shared cache, consider private. If storage itself is unwanted, consider no-store. Neither a login cookie nor HTTPS replaces a deliberate cache policy. These directives are not remote deletion commands for every previously saved copy.
Validators come in pairs
A browser holding article-v3 can ask whether it remains applicable:
GET /guide HTTP/1.1
If-None-Match: "article-v3"
For a normally successful conditional GET, a matching version allows a 304 response and reuse of the stored body. A changed version produces 200 with the new content. A 304 saves body transfer, but still involves a network round trip.
Modification time provides another validator:
Last-Modified: Tue, 08 Sep 2026 02:00:00 GMT
The subsequent request uses:
If-Modified-Since: Tue, 08 Sep 2026 02:00:00 GMT
| Response supplies | Validation request sends |
|---|---|
| ETag | If-None-Match |
| Last-Modified | If-Modified-Since |
Putting Last-Modified in Vary does not ask whether the content changed. Vary has another job. Also, an ETag does not force a request on every use: a fresh response can be reused directly when its policy permits.
See If-None-Match and If-Modified-Since for the request semantics.
Vary selects a matching representation
A URL may return different compressed representations according to Accept-Encoding. A response to a request accepting gzip might contain:
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Encoding describes the encoding of this response. Vary names request headers a cache must consider when matching stored responses. Cache-Control governs storage and reuse; Cache-Control: gzip is not a standard compression directive.
Similarly, a server selecting a language from Accept-Language can declare Vary: Accept-Language. That header does not translate anything. If /en/help and /zh-TW/help already identify separate content and do not vary with that request header, language Vary is unnecessary for that purpose. MDN Vary describes the matching behavior.
Does the HTML point at the new JavaScript?
Suppose the browser still has this HTML:
<script src="/main.aaa.js"></script>
Uploading /main.bbb.js does not make the browser discover it. The HTML must reference the new URL, and the browser must obtain that HTML. None of this requires a new domain.
Animated meme (expand/collapse)
Fingerprinted assets work well with long caching because changed content gets a different URL. For a public resource whose content stays unchanged at that URL, evaluate:
Cache-Control: public, max-age=31536000, immutable
This is not a site-wide setting. An image overwritten daily at a fixed URL, or frequently edited HTML, does not meet the same assumption.
Already-open pages matter during deployment. They may request an old chunk later. Deleting all old assets immediately can turn that request into a 404. Plan when new assets and HTML become available, and how long old assets remain accessible.
Jake Archibald’s caching article explores these deployment tradeoffs through concrete examples. It is a 2016 reference; the relevant lesson here is versioning and update coordination, not old tooling advice.
What to inspect when the page stays old
- Inspect the actual HTML, CSS, and JavaScript URLs in Network. Check whether the HTML references the expected build.
- Read Cache-Control, Age, ETag, Last-Modified, and Vary alongside the browser’s cache-source information. A 200 status alone does not establish that the origin supplied a new body.
- Test both a fresh browser and one that loaded the previous release before deployment. Do not validate only with Disable cache enabled.
- After purging the CDN, account for fresh responses already stored in browsers. Clearing one layer does not clear both.
What I learned
- I separate permission to store, permission to reuse directly, and the method of validation.
- I identify the responding layer before interpreting max-age, s-maxage, and Age.
- I distinguish validators from Vary: one checks content, the other determines which stored representation matches.
- I treat updated HTML references and old-asset retention as deployment work, rather than stopping after uploading new JavaScript.