Separate the page from its files

A small retailer may change a checkout button, a stylesheet and the script that calculates which delivery choices to show. To a returning visitor these are one release, but a browser sees separate URLs and separate HTTP responses. Caching each response by the same rule creates an avoidable mismatch: fresh HTML can point to an old script, or an old HTML document can keep pointing to a file that was removed. The first decision is therefore an inventory of response types. List public HTML, CSS, JavaScript, product images, fonts, and any data fetched after load. Record whether a URL can change its bytes in place. That property matters more than whether the file is called static.

MDN distinguishes a browser cache from a shared cache and explains that Cache-Control describes how a response may be stored or reused. A public product image identical for every visitor is a different case from an account page. This guide concerns only public assets; it does not propose a policy for personalised responses. For every public URL, identify who serves it: the origin, a content delivery network, or both. Ask whether a CDN can retain an older copy after the origin changes. The browser cannot know that a file with an unchanged address now contains different bytes while a previously cached response is still fresh. A release process must solve that identity problem explicitly.

A useful design note can be short: “HTML is revalidated; build assets have content-derived names; old build assets stay available during transition.” This is a proposed local policy, not a universal header recipe. It gives the developer, hosting provider and person checking the release the same model. It also reveals an unresolved case: an image uploaded under a stable public path that staff routinely replace. Either give that image a new URL when it changes, or use a shorter freshness policy and accept more validation. Do not silently give a mutable image the long life intended for content-addressed build files.

Make the asset URL identify its bytes

For files produced by a build, place a content hash or an equivalent changing version in the URL. A new stylesheet might be named shop.8f2c.css while the prior file remains shop.4a91.css. Those names are illustrative, not observed release identifiers. If the bytes change, the URL must change too; if the bytes do not change, the URL can remain stable. MDN calls this cache busting. A long max-age and immutable become defensible only when that promise is kept for the lifetime of the cached response. Merely appending a date while replacing the bytes at the same dated URL breaks the promise.

Versioning must cover dependencies as a set. If HTML refers to a new JavaScript bundle but an old stylesheet, ask whether their interfaces remain compatible. If a script dynamically imports another chunk, verify the referenced chunk is uploaded before any page points to it. If a build manifest maps logical names to hashed files, publish the assets first, then the HTML that names them. Keep prior hashed files long enough for visitors holding an earlier page to finish their session; the exact retention window depends on that site’s traffic and release rollback needs. Deleting all previous assets at deployment is particularly risky when an open tab later requests a lazy-loaded chunk.

Do not rely on a purge button as the main versioning system. A managed cache may offer purging, but MDN notes that HTTP caching does not generally provide a way for an origin to delete every previously stored response. Even a successful CDN purge does not erase a fresh copy already held by a customer browser. Treat purging as an operational recovery tool with its own verification, while new URLs carry the normal release. If hosting infrastructure rewrites or strips query strings, prefer a filename hash and test the actual edge behaviour rather than assuming the origin configuration reaches users unchanged.

Let returning visitors discover the new map

The HTML document is the map that tells a browser which versioned files to request. For a page that changes, a policy such as Cache-Control: no-cache asks caches to validate before reuse; it does not mean that the response cannot be stored. MDN explicitly distinguishes no-cache from no-store. With a validator such as ETag or Last-Modified, an unchanged page may receive a 304 response rather than a full transfer. Choose the policy with the host’s actual behaviour in mind. If the site is statically generated, check both the named page and any fallback route that serves the same HTML; a different CDN rule on a fallback path can undermine the plan.

Consider a worked release. At noon a customer opens a product page referencing app.A.js and style.A.css. At 12:10 the team publishes app.B.js and style.B.css, confirms both return successful responses, then switches the product HTML to the B URLs. The customer who opens a fresh page after validation receives the B map. The customer who has kept the noon tab open may still execute A and request another A chunk later, so A must remain available. No amount of changing the HTML response retroactively rewrites that open tab. If compatibility matters across an API change, design the server to tolerate both client versions for the transition.

A CDN can add another layer of policy. Inspect the headers and effective cache behaviour on the customer-facing hostname, not only the origin URL. A response may carry different Cache-Control directives or be served from a different location at the edge. Record who owns each header, whether the CDN overrides it, and how a preview or staging environment differs. The goal is to make the release observable: an operator should be able to name the HTML revision, follow its asset URLs, and confirm that the bytes at those URLs match the build. Avoid treating a browser’s hard refresh as proof that ordinary returning users will see the same result.

Test the return visit and the rollback

Build a test that starts before the deployment. Open the live page in an ordinary browser profile, let it cache the public assets, then deploy a harmless visible change and navigate normally in that same profile. In developer tools, inspect the HTML request, the resolved asset URLs, response status, Cache-Control, and whether an asset came from memory, disk or the network. Repeat through the public domain on a second device if the CDN has geographically different edges. Record the exact URL and response you observed. This is more useful than a broad assertion that the cache works, because it separates stale HTML, missing chunks and an incorrect header.

Test a rollback with the same care. If HTML is reverted to the A map, A files must still be reachable and retain their original bytes; reusing B filenames for A content would violate the identity rule. Check the page in an existing tab as well as a new navigation. If a genuinely bad public asset was distributed under a long-lived immutable URL, issue a corrected URL and update the HTML rather than overwriting the bad bytes at that address. A purge may assist at the CDN, but it cannot guarantee removal from every browser. Include this recovery step in the deployment checklist before choosing a long freshness period.

Finally, schedule a small review after the release rather than copying headers indefinitely. Check that deployment cleanup has not removed files still referenced by old tabs, and that images edited by staff are not accidentally placed in the immutable build path. Compare the configured policy with a captured response from the public hostname. If a site uses a service worker, its own cache and update lifecycle adds another layer; this HTTP guide does not replace that separate review. The practical acceptance criterion is specific: a returning visitor obtains current HTML on navigation, every referenced public file exists with stable bytes, and a held open page can complete its current journey.

Before you proceed

  • Can we identify which public URLs may change in place?
  • Do HTML responses revalidate while hashed files keep stable bytes and remain available across the transition?
  • Have we tested an ordinary return visit, a held open tab and a rollback on the public hostname?

Sources & further reading