Updated April 2026

Cache-Control Headers in Nginx — Complete Configuration Guide

Quick Answer

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 reload
Generate Cache-Control for your stack →