Browser caching lets visitors reuse files they’ve already downloaded — stylesheets, scripts, images, fonts — instead of fetching them on every page. Repeat views load much faster, and your server sends far less data. In Nginx it’s a handful of lines; the important part is choosing the right lifetime for each kind of file.

The two kinds of static files

  1. Versioned (fingerprinted) assets — filenames include a content hash, like app.3f9a2c1b.js (Vite, webpack, Astro, Next.js, Laravel Mix…). When content changes, the name changes. These can be cached for a year and marked immutable.
  2. Unversioned files — logo.png, style.css?ver=6.6, uploaded images. They might change under the same URL, so cache them for days or weeks, or rely on query-string versioning.

HTML pages should usually be revalidated on each visit, so users see new content and new asset references.

Configuration

server {
    # …

    # Fingerprinted build assets: one year, immutable
    location ~* ^/(assets|_next/static|build)/ {
        expires 1y;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
        try_files $uri =404;
    }

    # Other static files: 30 days
    location ~* \.(?:css|js|mjs|png|jpe?g|gif|webp|avif|svg|ico|woff2?|ttf|otf|mp4|webm|pdf)$ {
        expires 30d;
        add_header Cache-Control "public";
        access_log off;
        try_files $uri =404;
    }

    # HTML: always revalidate
    location ~* \.html$ {
        add_header Cache-Control "no-cache";
    }
}
  • expires 30d sets both Expires and Cache-Control: max-age=2592000.
  • immutable tells browsers not to even revalidate during the lifetime — ideal for hashed files.
  • no-cache doesn’t mean “don’t store”; it means “check with the server before using”. With ETags, that check is a tiny 304 Not Modified response.
  • access_log off reduces log noise from asset requests.

Adapt the first location’s path to where your build tool puts hashed files.

A note on add_header inheritance

add_header directives are only inherited from the parent level if the current block defines none of its own. Adding Cache-Control in a location drops security headers set at the server level for those responses. Repeat them in the location, or put them in a snippet you include everywhere. See Nginx security headers.

ETags and Last-Modified

Nginx sends ETag and Last-Modified for static files by default, enabling cheap revalidation (304). Leave them on. If several servers serve the same files behind a load balancer, make sure file modification times match (deploy with rsync -a) so ETags are consistent.

Server-side: open_file_cache

Separate from browser caching, Nginx can cache file descriptors and metadata for frequently served files:

http {
    open_file_cache max=10000 inactive=60s;
    open_file_cache_valid 120s;
    open_file_cache_min_uses 2;
    open_file_cache_errors on;
}

Helpful for sites serving many static files at high request rates.

Compression goes hand in hand

Cache and compress text assets — see gzip and Brotli in Nginx.

WordPress and query-string versions

WordPress adds ?ver=6.6.2 to CSS and JS URLs. The query string changes with updates, so 30-day (or longer) caching is safe for these files. Uploaded images rarely change under the same name, so long lifetimes are fine for wp-content/uploads too.

CDN considerations

If Cloudflare or another CDN sits in front, your Cache-Control headers also guide the CDN’s edge cache. Long lifetimes on hashed assets let the CDN serve them without asking your server. Purge the CDN when you replace unversioned files.

Test it

curl -sI https://example.com/assets/app.3f9a2c1b.js | grep -iE 'cache-control|expires|etag'
curl -sI https://example.com/ | grep -i cache-control

In browser developer tools (Network tab), reloaded assets should show “(memory cache)” or “(disk cache)”, or 304 responses for revalidated files. Lighthouse’s “Serve static assets with an efficient cache policy” audit flags files with short lifetimes.

Common mistakes

  • Long caching on unversioned CSS/JS → users see stale files after a deploy. Add versioning or shorten lifetimes.
  • Caching HTML for long → visitors get old pages referencing deleted assets.
  • Setting Cache-Control: private, no-store on everything via a framework or plugin → nothing is cached.

Related: HTTP caching headers explained and website speed optimization.

  • Nginx
  • Caching
  • Performance