When a visitor loads a page, their browser downloads the HTML, then the stylesheets, scripts, fonts and images the page uses. Most of those files do not change between visits. If your server tells the browser it may keep them, a returning visitor skips most of those downloads and the page appears sooner. The instruction that does this is the Cache-Control response header.
It is easy to get wrong in two opposite directions. Set nothing, and browsers guess how long to keep each file. Set a very long lifetime on a file whose name never changes, and visitors can be stuck with an old stylesheet after you update the site. This guide shows a safe pattern: long lifetimes for files whose names change when their content changes, and cautious rules for HTML and everything personal.
It is for site owners and developers who have seen a "serve static assets with an efficient cache policy" warning, or who want to set caching deliberately. By the end you will have a rule for each file type, the header values to use, and checks to prove they work. Caching speeds up repeat visits. It does not improve the first visit and it carries no promise of any ranking change.
Before you begin
You need access to where headers are set: your web server configuration, a hosting control panel, your application framework, or your CDN. You need a way to see response headers: the Network panel in browser Developer Tools, or curl -I.
The key question is whether your static file names change when the content changes (for example app.3f9a1c.css). If your build tool adds a hash or version, you can cache aggressively. If it does not, you must either add versioning or use shorter lifetimes. The risk of getting this wrong is visitors seeing outdated files, so test on a staging copy or on one file type first, and know how to purge your CDN. Back up the current configuration.
How the headers work
MDN documents these Cache-Control directives, and they are the ones to know:
max-age=Nmeans the response stays fresh for N seconds after it was generated. During that time the browser can reuse it without asking the server.s-maxage=Nis the same idea for shared caches such as CDNs. It overridesmax-agefor them, and browsers ignore it.no-cachedoes not mean "do not cache". The response may be stored, but it must be revalidated with the server before each reuse.no-storemeans no cache of any kind may store the response. Use it for content that must never persist.privatemeans only the visitor's own browser cache may store the response. Use it for personalized content.publicallows shared caches to store the response, including when the request carried anAuthorizationheader.immutablesays the response will not change while it is fresh, so clients need not revalidate, even on reload.stale-while-revalidate=Nlets a cache reuse a stale response for N more seconds while it refreshes in the background.
Revalidation uses validators. The server sends an ETag (or Last-Modified). On the next request the browser sends it back in If-None-Match (or If-Modified-Since). If nothing changed, the server answers 304 Not Modified with no body, which is very small. web.dev calls ETag the more accurate of the two.
Importantly, if you send no Cache-Control header at all, caching is not off. MDN warns that caches are allowed to store the response heuristically, so the result can be unpredictable. State your intent explicitly.
Step-by-step instructions
Step 1: Inventory what your pages load
Open DevTools, tick "Disable cache", reload, and look at the Network panel. Group the requests: HTML documents, CSS and JavaScript, images, fonts, and API or data requests. For each, note whether the file name contains a version or hash and what Cache-Control header it currently has (click a request and read the response headers).
Crawvix has a check named "Static files have cache headers". It requests up to six static files that your home page loads from your own domain and judges their headers. A file passes with Cache-Control of at least one day (max-age or s-maxage of 86,400 seconds or more) or with immutable. It is flagged when the header contains no-cache or no-store or when no lifetime is given. The check looks at a handful of files, so use your own inventory for the whole site.
What success looks like: You have a list of file groups with current headers and a note on which ones are versioned.
Step 2: Version your static files
Long lifetimes are only safe if a changed file gets a new URL. web.dev's advice: embed a fingerprint or version in the file name, such as style.x234dff.css, so that updating the file changes its address and visitors fetch the new one immediately. Most build tools can generate hashed names. If you edit CSS or JavaScript by hand on a simple site, a query string like style.css?v=12 is a weaker option than a changed file name, because it only works if every cache in the path treats the new URL as a new file. Change the value on each release if you use it.
Unversioned files, like logo.png that you replace without renaming, need shorter lifetimes.
What success looks like: CSS and JavaScript references in your HTML include a version in the name or the URL, and change when the file changes.
Step 3: Give versioned files a long lifetime
For versioned static files, MDN and web.dev both recommend a one-year lifetime. web.dev notes that 31,536,000 seconds is the maximum supported value, and says you can add immutable as an extra optimization, though some browsers ignore it.
Cache-Control: public, max-age=31536000, immutable
Here is a sample for nginx, restricted to a folder that holds only fingerprinted files. Adjust the path to your own setup and check your server's documentation for the equivalent in Apache, IIS or your hosting panel.
location /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
Do not apply this rule to everything under / or to file types such as .html.
What success looks like: A versioned CSS file returns the header above, and a second page load takes it from the browser cache.
Step 4: Handle HTML and unversioned files with revalidation
HTML pages almost never carry versions in their URLs, so you cannot force browsers to fetch a new copy if you cache them for a long time. web.dev says you can't do cache busting without changing the URL, so users may see stale content. For HTML, MDN's example is Cache-Control: no-cache, which stores the page but checks with the server each time, paired with an ETag so most checks return a small 304.
Cache-Control: no-cache
ETag: "33a64df5"
For unversioned images and other files that change rarely, choose a moderate lifetime such as a day or a week, plus ETag, and accept that changes appear after that period. If you want a shared cache to hold a page briefly, a short s-maxage is possible, but only for content that is the same for everyone.
What success looks like: HTML revalidates on each load, and unversioned files have a lifetime you are comfortable with.
Step 5: Protect personal and sensitive responses
Pages that depend on the visitor (account pages, carts, dashboards) must not end up in a shared cache. MDN's example for personalized responses is Cache-Control: private, no-cache. Use no-store for anything that must not be stored at all. Omitting private on personalized content can let a shared cache serve it to other visitors.
Cache-Control: private, no-cache
Be aware of one limitation: MDN says no-cache, must-revalidate and max-age do not always force revalidation when a visitor uses the back and forward buttons, because the browser may restore a saved snapshot of the page. If a page shows sensitive data, do not rely on revalidation alone.
What success looks like: Logged-in pages return private or no-store, and no personal page appears in your CDN cache.
Step 6: Add the validators search engines use
Google's crawler documentation says its crawlers support ETag with If-None-Match and Last-Modified with If-Modified-Since. When both are present they use ETag, and Google recommends setting ETag. Google also says that Cache-Control: max-age is optional but can indicate how long content stays unchanged, and that other caching directives are not supported by its crawlers. In practice that means your browser caching headers and your crawler-facing validators can live side by side. Make sure Last-Modified uses an RFC-compliant date format.
This helps crawlers avoid downloading unchanged files. It does not make Google crawl more.
What success looks like: Pages and static files send ETag or Last-Modified, and a conditional request returns 304.
Step 7: Verify with real requests
Run these checks:
curl -I https://example.com/assets/app.3f9a1c.css
curl -I -H "If-None-Match: \"33a64df5\"" https://example.com/
The first should show your long Cache-Control. The second, using an ETag copied from a real response, should return 304 Not Modified if the page is unchanged. In DevTools, untick "Disable cache", reload twice, and look at the Size column. Chrome marks files served from its cache in the Size column, for example as memory cache or disk cache. Finally, change a versioned file, deploy, and confirm browsers fetch the new name.
What success looks like: Versioned files are served from cache on repeat visits and update immediately after a deploy.
Final verification checklist
- CSS, JavaScript and font files have hashed or versioned names.
- Versioned static files send a long
max-age, and optionallyimmutable. - HTML sends
no-cacheor a deliberately chosen lifetime, with anETag. - Personal pages send
privateorno-store. - No
Cache-Controlheader is missing on files you care about. - A conditional request returns 304 for unchanged content.
- After a deploy, visitors receive the new file names, not old cached ones.
- The CDN, if you use one, follows the same rules.
Common mistakes to avoid
Using a long max-age on unversioned URLs. Visitors cannot be forced to refresh. Add versioning or shorten the lifetime.
Thinking no-cache means "do not store". It means store and revalidate. For no storage, use no-store.
Leaving Cache-Control unset. Browsers and caches then guess. Say what you mean.
Applying one blanket rule to the whole site. HTML, images, APIs and downloads need different treatment.
Caching personalized responses publicly. Always mark them private or no-store.
Relying on immutable alone. Some browsers ignore it, so keep a sensible max-age too.
Bundling frequently changing code with stable libraries. Any change invalidates the whole file. Split them so stable files stay cached.
Forgetting the CDN. A CDN may have its own caching rules that override or ignore your headers. Check them.
Frequently asked questions
What is the best Cache-Control value for images?
If the file name is versioned, a long lifetime like public, max-age=31536000, immutable works. If images are replaced without renaming, use a moderate lifetime and an ETag.
How long should I cache CSS and JavaScript?
For fingerprinted files, up to a year. For files without a version, only as long as you are happy for visitors to see old versions.
Does Cache-Control affect SEO?
It affects speed on repeat visits, and Google says its crawlers support ETag and Last-Modified. It does not directly change how pages rank, and this guide makes no ranking claim.
Do I need Expires as well?
Not if you send Cache-Control: max-age. A CDN needs at least one of max-age, s-maxage or Expires. Many sites use Cache-Control only.
Why do I still see old files after a deploy?
Either the file name did not change while a long lifetime was set, or a CDN holds a copy. Version the name, or purge the CDN. See CDN setup.
Related guides
- CDN for SEO: Set Up a CDN Without Breaking Crawling
- Time to First Byte (TTFB): Measure It and Fix a Slow Server
- Brotli and Gzip Compression: Turn It On and Verify It
- Improve Lighthouse Performance Without Breaking Your Website
- Add HTTP Security Headers: CSP, nosniff, Referrer-Policy and More
Official references
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control
- https://web.dev/articles/http-cache
- https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
- https://web.dev/articles/content-delivery-networks
Next step: See how Crawvix checks a website and capture a baseline once scanning opens. This guide provides technical information, not a guarantee of rankings, traffic or index inclusion.
Crawvix Editorial Team
· Published October 29, 2026 · Last updated October 29, 2026 · 9 min read