Static assets rarely change, so tell the browser to keep them. A regex location matching common extensions does it.
location ~* \.(?:css|js|woff2?|ttf|otf|eot|ico|gif|png|jpe?g|webp|avif|svg|mp4)$ {
expires 30d;
add_header Cache-Control "public";
access_log off; # optional: stop logging every asset hit
try_files $uri =404;
}
For content-hashed filenames (e.g. app.4f3a1c.js from a bundler) you can cache forever, because a change ships a new filename:
location ~* \.(?:css|js|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
Notes:
expiressets both theExpiresandCache-Control: max-ageheaders.expires 1y;=max-age=31536000.immutabletells modern browsers not to even revalidate during the lifetime — no conditional request on reload. Only safe for filenames that change when content changes.~*is a case-insensitive regex match. Regex locations are checked before plain prefixes, so this catches assets regardless of which folder they’re in. If you keep all assets under one path, a prefixlocation /assets/ { … }is cheaper than a regex.- For HTML, do the opposite — short or no cache (
Cache-Control "no-cache"), so content updates show up immediately.
Check it landed:
curl -sI https://example.com/app.4f3a1c.js | grep -i cache-control
# cache-control: public, immutable