You pushed a CSS change, but some users still see the old design. The usual cause is HTTP caching working exactly as configured, just not the way you intended. This article explains how Cache-Control works, why stale files happen, and how to set headers so users always get fresh content without giving up speed.
What Cache-Control does
When a browser downloads a file, the server can attach a Cache-Control header telling the browser how long it may reuse that file without asking again. This is why a returning visitor loads your page faster: the browser serves CSS, JavaScript, and images from its local cache instead of re-downloading them.
The trade-off is obvious once you hit it. If you told the browser to cache style.css for a year, and you change that file, the browser keeps using its cached copy until the year passes or the cache is cleared. The user sees old styles.
The directives you actually use
- max-age=SECONDS — how long the file is considered fresh.
max-age=31536000is one year. - no-cache — the browser may store the file but must revalidate with the server before using it. It does not mean “do not store.”
- no-store — do not cache at all. Use for private or sensitive responses.
- immutable — tells the browser the file will never change, so it should not revalidate even on reload.
- public / private — whether shared caches (like a CDN) may store it, or only the user’s browser.
The pattern that solves stale files: cache busting
The reliable approach is to cache aggressively but change the filename whenever the content changes. This is called cache busting or fingerprinting.
Instead of style.css, your build outputs style.a1b2c3.css, where the hash comes from the file content. When you change the CSS, the hash changes, so the filename changes, so the browser treats it as a brand new file and downloads it. The old cached copy is simply never requested again.
This lets you set a one-year max-age with confidence. You get maximum caching speed and instant updates, because updates arrive as new URLs.
A real scenario
A team reported that after every deploy, roughly a third of users complained about a broken layout for a day, then it fixed itself. Their server sent Cache-Control: max-age=86400 on main.css, a plain filename. Each deploy changed the file content but not the name, so browsers kept the previous version for up to 24 hours.
The fix had two parts. First, their build tool added content hashes to filenames. Second, they split headers by type: hashed assets got max-age=31536000, immutable, while the HTML document got no-cache so the browser always revalidates and picks up the newest asset filenames. After that, deploys took effect immediately for everyone.
Which header for which file
| File type | Recommended Cache-Control |
| HTML documents | no-cache (revalidate every time) |
| Hashed CSS / JS / fonts | max-age=31536000, immutable |
| Non-hashed CSS / JS | short max-age or no-cache |
| User-specific / sensitive data | no-store, private |
Common mistakes and how to fix them
- Long max-age on plain filenames. This is the classic stale-CSS bug. Fix: add content hashes to asset filenames before caching them for a long time.
- Assuming no-cache means no caching. It means revalidate, not skip storage. Fix: use
no-storewhen you truly want nothing cached. - Caching the HTML document for a long time. If the HTML is cached, users never see new asset URLs. Fix: keep HTML on
no-cacheor a very short max-age. - Forgetting the CDN layer. A CDN has its own cache. Even correct browser headers will not help if the CDN serves an old file. Fix: purge the CDN on deploy or rely on hashed URLs the CDN treats as new.
- Caching authenticated pages publicly. Using
publicon private data can leak it into shared caches. Fix: mark private responses withprivateorno-store.
Action checklist
- Enable content hashing for CSS, JS, and fonts in your build tool.
- Set hashed assets to
max-age=31536000, immutable. - Set HTML documents to
no-cache. - Use
no-storefor sensitive or user-specific responses. - Purge or verify your CDN cache on each deploy.
- Confirm headers in your browser’s network panel after deploying.
Conclusion and next step
Stale files are almost always a caching configuration choice, not a browser bug. Cache hashed assets aggressively, keep HTML fresh, and let changed content arrive as new URLs. Next step: open your network panel, check the Cache-Control header on your CSS, and confirm your filenames include a content hash.
FAQ
Why do only some users see the old version?
Caching is per browser. Users who visited recently hold a cached copy, while new or cache-cleared users get the fresh file. That is why the problem looks random.
What is the difference between Cache-Control and ETag?
Cache-Control sets how long a file is fresh. An ETag is a fingerprint the browser sends back to ask “has this changed?” during revalidation. They work together: Cache-Control controls freshness, ETag controls efficient revalidation.
Is a hard refresh a real fix?
It only helps the one person who does it. Telling every user to hard refresh is not a solution. Cache busting fixes it for everyone automatically.
Should I ever use no-store on assets?
Rarely for static assets, since it removes all caching benefit. Reserve no-store for private or sensitive responses that must never be reused.
References
- MDN Web Docs: the
Cache-ControlHTTP header and its directives. - MDN Web Docs: HTTP caching, ETag, and revalidation.