Good caching makes repeat visits nearly instant, and cuts server load and bandwidth. Bad caching either wastes that opportunity, or serves stale content after a deploy. The behaviour is controlled by a few HTTP response headers. Here’s what each does, and sensible settings for typical sites.

The main headers

Header Purpose
Cache-Control The modern, primary caching instruction
Expires Older absolute expiry date (ignored when Cache-Control: max-age exists)
ETag A fingerprint of the response, for revalidation
Last-Modified Modification date, for revalidation
Vary Which request headers change the response (e.g. Accept-Encoding, Accept)

Cache-Control directives

Directive Meaning
max-age=N Fresh for N seconds (browsers and shared caches)
s-maxage=N Freshness for shared caches (CDNs, proxies) only
public May be stored by shared caches
private Only the user’s browser may store it (personalised pages)
no-cache May be stored, but must be revalidated before each use
no-store Never store (sensitive data)
immutable Will never change during its lifetime: don’t even revalidate
stale-while-revalidate=N Serve stale for N seconds while refreshing in the background

no-cache doesn’t mean “don’t cache”. It means “check with the server first”. Use no-store for truly sensitive responses.

Revalidation: ETag and Last-Modified

When a cached copy is stale (or no-cache), the browser asks: “has this changed?” (If-None-Match: <etag>, or If-Modified-Since). If not, the server replies 304 Not Modified, with no body. That’s fast and cheap. Nginx sends ETag and Last-Modified for static files automatically.

A practical strategy

Content Cache-Control
Hashed assets (app.4f2a1c.js, style.9b8e.css) public, max-age=31536000, immutable
Unhashed static files (images, fonts) public, max-age=2592000 (30 days)
HTML pages no-cache (always revalidate), or a short max-age with stale-while-revalidate
Logged-in / personalised pages private, no-cache
Sensitive responses (account data, tokens) no-store
APIs Usually no-store or short max-age, depending on the data

The key trick: hashed file names (cache busting). Most build tools (Vite, webpack, Astro, Django ManifestStaticFilesStorage, Laravel Mix/Vite) add a content hash, so you can cache assets “forever”, and a deploy simply references new names. See Django static files.

Nginx configuration

# hashed build assets
location /assets/ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
    access_log off;
}

# images and fonts
location ~* \.(png|jpe?g|gif|webp|avif|svg|ico|woff2?)$ {
    expires 30d;
    add_header Cache-Control "public, max-age=2592000";
}

# HTML: always revalidate
location / {
    add_header Cache-Control "no-cache";
    try_files $uri $uri/ /index.html;
}

Note: add_header inside a location replaces headers inherited from the server block. Repeat security headers there, or use an include file. See Nginx static file caching and Nginx security headers.

CDNs

CDNs follow Cache-Control (and s-maxage), though many have their own rules, and don’t cache HTML by default. Purge the CDN after deploys if HTML is edge-cached. See CDN for websites and WordPress CDN setup.

Test your headers

curl -sI https://example.com/assets/app.4f2a1c.js | grep -iE 'cache-control|etag|last-modified|expires|age'
curl -sI https://example.com/ -H 'If-None-Match: "<etag>"' | head -1      # expect 304

In the browser’s DevTools Network tab, the “Size” column shows “(disk cache)” or “(memory cache)” for cached responses. Lighthouse flags “Serve static assets with an efficient cache policy”. See web performance testing.

Common mistakes

  • Long max-age on unhashed CSS/JS: users keep old files after a deploy.
  • Caching personalised HTML as public: a CDN may show one user’s page to another.
  • no-store everywhere: no caching benefit at all.
  • Forgetting Vary when responses differ by header (compression, image format).

Quick reference

Goal Header
Cache forever (hashed) public, max-age=31536000, immutable
Always check freshness no-cache
Never store no-store
Browser only private
CDN-specific lifetime s-maxage=N
Revalidation ETag / Last-Modified → 304

Related: website speed optimization and minify CSS and JS.

  • Performance
  • Nginx
  • CDN