A practical guide to debugging stale assets, cache headers and broken deployments.
Your deployment finishes successfully. The homepage loads, the navigation works and the console looks clean.
Then a returning visitor reports a blank page.
You open the website in an incognito window. Everything works again.
This difference is a useful clue: the two browser sessions may be loading different combinations of HTML, JavaScript and cached responses.
Caching makes websites faster, but a deployment must account for visitors who already have an older version open.
This tutorial uses an illustrative application to explain how these failures happen and how to investigate them.
Two Ways a Deployment Can Break
Consider an application with these files:
/index.html
/assets/app.js
The HTML loads the JavaScript:
<script type="module" src="/assets/app.js"></script>
Suppose app.js receives a long cache lifetime. You deploy new HTML and overwrite the JavaScript at the same URL.
A returning visitor may receive the updated HTML while their browser reuses the old JavaScript. If the new markup requires behavior introduced in the new script, the application can fail.
A second failure occurs with versioned filenames:
Release A: /assets/app.a1b2c3.js
Release B: /assets/app.d4e5f6.js
If deployment B deletes release A’s assets immediately, an old browser tab may later request a lazy-loaded chunk from release A and receive a 404.
These failures need different fixes. Start by identifying which versions the browser actually received.
1. Reproduce the Returning Visitor’s Experience
Clearing the cache at the beginning removes evidence.
Use a browser profile with an existing visit to the application. Open DevTools before reproducing the problem.
In the Network panel:
- Leave Disable cache unchecked for the initial reproduction.
- Enable Preserve log.
- Reload normally.
- Inspect the document and JavaScript requests.
- Reproduce the action that fails.
Look for:
- Responses served from memory or disk cache
- Requests handled by a service worker
- Missing JavaScript chunks
- Unexpected HTML responses for JavaScript URLs
- Asset filenames associated with different releases
Then repeat with the HTTP cache disabled and compare the behavior. Service-worker caches require separate investigation.
2. Inspect the Headers Actually Delivered
Check the public URL, including any CDN in front of the application:
curl -sS -D - -o /dev/null \
"https://example.com/"
Repeat for the affected asset:
curl -sS -D - -o /dev/null \
"https://example.com/assets/app.a1b2c3.js"
These commands make GET requests, print response headers and discard response bodies. They do not reproduce the browser’s stored cache or execute JavaScript.
Inspect:
Cache-Control
ETag
Last-Modified
Age
Content-Type
CDNs may also provide diagnostic headers indicating cache hits or misses.
An Age header can help identify a response served through a shared cache, but a missing header does not prove the response came directly from the origin.
Also check the response body when something looks wrong. A JavaScript request returning 200 OK with text/html may be receiving the application’s fallback page rather than the expected asset.
3. Give HTML and Versioned Assets Different Policies
For frequently updated public HTML, a common starting point is:
Cache-Control: no-cache
Despite its name, no-cache allows storage. It requires successful validation before reuse. Validators such as ETag can let the server confirm that an existing copy remains valid.
By comparison:
Cache-Control: no-store
instructs caches not to store the response.
For public static assets whose URLs change whenever their content changes, a common policy is:
Cache-Control: public, max-age=31536000, immutable
The crucial requirement is that the content at that URL stays unchanged. These policies must also account for personalized responses and any CDN rules.
4. Use Content Hashes in Asset Filenames
Instead of repeatedly overwriting:
/assets/app.js
generate a new filename when the file changes:
/assets/app.a1b2c3.js
/assets/app.d4e5f6.js
The new HTML references the new file:
<script
type="module"
src="/assets/app.d4e5f6.js"
></script>
A different URL gives the browser a separate cache entry. Modern build tools can generate these hashes and update references automatically.
Verify that the build covers imported chunks, CSS and other dependent resources—not only the entry script.
5. Deploy Assets Before Publishing Their References
A safer release sequence is:
- Build the application.
- Upload the new versioned assets.
- Verify that those assets are available.
- Publish the HTML or manifest that references them.
- Refresh relevant cached HTML.
- Retain earlier assets for an appropriate compatibility window.
The retention window depends on how long users keep tabs open, how rollback works and how the application loads additional code.
Changing cache headers today cannot reliably

Top comments (0)