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
- 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. - 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 30dsets bothExpiresandCache-Control: max-age=2592000.immutabletells browsers not to even revalidate during the lifetime — ideal for hashed files.no-cachedoesn’t mean “don’t store”; it means “check with the server before using”. With ETags, that check is a tiny304 Not Modifiedresponse.access_log offreduces 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-storeon everything via a framework or plugin → nothing is cached.
Related: HTTP caching headers explained and website speed optimization.