Cache-Control Headers in Nginx — Complete Configuration Guide
Use add_header Cache-Control "public, max-age=31536000, immutable" always; inside a location block for static assets. Use no-cache for HTML. Use no-store for authenticated API responses. The always flag ensures headers appear on error responses too.
Nginx does not set Cache-Control headers by default. Every asset serves without caching instructions — browsers and CDNs make their own decisions. This guide shows the exact location blocks for each asset type.
Generate Cache-Control headers →Static assets — hashed filenames
JavaScript, CSS, fonts, and images with fingerprinted filenames (e.g. app.a3f9d2c.js) — cache forever. The filename changes on deploy, so browsers never get stale files.
location ~* \.(js|css|woff2|woff|ttf|png|jpg|jpeg|gif|ico|svg|webp)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
add_header Vary "Accept-Encoding" always;
etag off;
expires 1y;
}
HTML pages
HTML must always revalidate so browsers pick up the latest asset URLs after a deploy.
location ~* \.html$ {
add_header Cache-Control "no-cache" always;
add_header Vary "Accept-Encoding" always;
}
# Or for all locations serving HTML:
location / {
add_header Cache-Control "no-cache" always;
try_files $uri $uri/ /index.html;
}
API responses — public
location /api/public/ {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=300" always;
add_header Vary "Accept, Accept-Encoding" always;
proxy_pass http://backend;
}
API responses — authenticated
location /api/ {
add_header Cache-Control "no-store" always;
add_header Pragma "no-cache" always;
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Complete server block example
server {
listen 443 ssl;
server_name example.com;
# Static assets — cache forever (hashed filenames only)
location ~* \.(js|css|woff2|png|jpg|svg|ico)$ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
etag off;
expires 1y;
}
# HTML — always revalidate
location / {
add_header Cache-Control "no-cache" always;
try_files $uri $uri/ /index.html;
}
# Authenticated API — never cache
location /api/ {
add_header Cache-Control "no-store" always;
proxy_pass http://localhost:3000;
}
}
The always flag — why it matters
Without always, Nginx only adds the header on 2xx responses. Error responses (4xx, 5xx) won't include Cache-Control — meaning CDNs might cache error pages. Always use always.
# Wrong — missing on error responses add_header Cache-Control "no-store"; # Correct — present on all responses including errors add_header Cache-Control "no-store" always;
Test your headers
# Check what Cache-Control your server is returning curl -sI https://example.com/app.js | grep -i cache # After changes: test config then reload nginx -t && nginx -s reloadGenerate Cache-Control for your stack →