Websites & apps

Cache-Control headers explained for CDNs and browsers

Control how browsers and CDNs cache your site with Cache-Control: max-age, s-maxage, no-cache vs no-store, immutable, stale-while-revalidate, ETags, and example rules for Nginx, Apache and .htaccess.

IntermediateWebsites2 min read

Browsers and CDNs decide what to cache, and for how long, mostly from the Cache-Control header. Getting it right makes repeat visits fast and offloads your server; getting it wrong serves stale pages or caches private data.

Before you start

  • Access to your web server config, or .htaccess on cPanel hosting.
  • Background: what is a CDN.

Key directives

DirectiveMeaning
max-age=NFresh for N seconds in browsers and shared caches
s-maxage=NOverrides max-age for shared caches (CDNs, proxies)
public / privateWhether shared caches may store it; private = browser only
no-cacheMay store, but must revalidate before each use
no-storeNever store (for sensitive data)
immutableWill never change during max-age; skip revalidation
stale-while-revalidate=NServe stale for N seconds while fetching a fresh copy

A sensible strategy

  • Fingerprinted assets (e.g. app.3f9a1c.js): public, max-age=31536000, immutable.
  • Images and fonts without hashes: a week or a month, e.g. public, max-age=604800.
  • HTML pages: short or revalidated, e.g. public, max-age=0, s-maxage=300, stale-while-revalidate=60 so the CDN caches briefly.
  • Logged-in pages, carts, account data: private, no-store.

Validators: ETag and Last-Modified

When a cached copy expires, the client asks "has it changed?" with If-None-Match or If-Modified-Since. If not, the server returns 304 Not Modified with no body, saving bandwidth.

Nginx example

location ~* \.(css|js|woff2|svg|png|jpg|webp|avif)$ {
    add_header Cache-Control "public, max-age=2592000";
}

For hashed build files, use a separate location with one year and immutable. See Nginx performance tuning.

Apache / .htaccess example

<IfModule mod_headers.c>
  <FilesMatch "\.(css|js|woff2|svg|png|jpg|webp)$">
    Header set Cache-Control "public, max-age=2592000"
  </FilesMatch>
</IfModule>

On LiteSpeed servers with WordPress, the LiteSpeed Cache plugin sets many of these for you; see LiteSpeed Cache plugin.

Check headers

curl -sI https://example.com/assets/app.css | grep -i -E 'cache-control|age|cf-cache-status|x-cache'

CDN headers such as Age or a cache status header show whether the response came from cache.

Troubleshooting

  • Users see old CSS after a deploy: long max-age on unversioned files; use fingerprinted filenames or purge the CDN.
  • Private data cached by CDN: missing private/no-store, or the CDN is set to cache everything; fix headers and CDN rules.
  • Header appears twice: both the app and server set it; keep one source.

Frequently asked questions

no-cache or no-store?

no-cache allows storing with revalidation; no-store forbids storing at all.

Does Expires still matter?

Cache-Control max-age takes precedence; Expires is a legacy fallback.

Do CDN page rules override headers?

Many CDNs can override origin headers with their own rules; check both.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.