How to Properly Configure Cache-Control Headers for Static Assets in Nginx: Solving Mismatched max-age Issues


2 views

When examining the HTTP response headers for static assets like facebook.png, we noticed something peculiar - the max-age value was set to 120 seconds instead of the expected 86400 seconds (1 day) we configured in our Nginx settings. Additionally, our custom X-Asset header was completely missing.

location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
    expires 1d;
    access_log off;
    add_header Pragma public;
    add_header Cache-Control "public, max-age=86400";
    add_header X-Asset "yes";
}

After thorough investigation, I discovered that Nginx has specific behavior regarding header inheritance. The issue stems from these key factors:

  • Nginx processes directives in a hierarchical manner
  • Header directives aren't inherited by default
  • The expires directive might create its own Cache-Control header
  • Multiple add_header directives in different blocks can override each other

Here's the corrected configuration that ensures proper Cache-Control headers:

server {
    # ... other server config ...

    # Static assets location block
    location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
        expires 1d;
        access_log off;
        add_header Pragma public;
        add_header Cache-Control "public, max-age=86400";
        add_header X-Asset "yes";
        
        # Important: prevent inheritance issues
        add_header Last-Modified "";
        
        # Ensure no conflicting expires directive
        etag on;
        
        try_files $uri =404;
    }

    # ... remaining server config ...
}

The solution involves several important adjustments:

  1. We explicitly set Last-Modified to empty to prevent default header generation
  2. Added etag on to ensure proper cache validation
  3. Included try_files to handle missing files properly
  4. Made sure the location block matches all static asset extensions

After applying these changes, restart Nginx and verify with curl:

curl -I https://www.example.com/icons/facebook.png

You should now see the correct headers:

HTTP/1.1 200 OK
Server: nginx/1.11.2
Date: Wed, 05 Oct 2016 09:15:22 GMT
Content-Type: image/png
Content-Length: 416
Last-Modified: Mon, 03 Oct 2016 18:18:37 GMT
Connection: keep-alive
ETag: "57f2a0fd-1a0"
Expires: Thu, 06 Oct 2016 09:15:22 GMT
Cache-Control: public, max-age=86400
Pragma: public
X-Asset: yes

For more complex setups, consider these additional optimizations:

# Map file extensions to cache durations
map $request_uri $cache_control {
    default "public, max-age=600";
    "~*\.(ico|css|js)$" "public, max-age=31536000, immutable";
    "~*\.(gif|jpeg|jpg|png|svg)$" "public, max-age=86400";
    "~*\.(woff|ttf|otf|woff2|eot)$" "public, max-age=604800";
}

server {
    # ... server config ...
    
    location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
        expires max;
        add_header Cache-Control $cache_control;
        add_header X-Asset "yes";
        access_log off;
    }
}

When examining the HTTP response headers for static assets on my Nginx server, I noticed the Cache-Control settings weren't being applied as expected. Despite configuring:

location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
    expires 1d;
    access_log off;
    add_header Pragma public;
    add_header Cache-Control "public, max-age=86400";
    add_header X-Asset "yes";
}

The actual response showed a max-age of 120 seconds instead of the intended 86400 (1 day):

Cache-Control:public, max-age=120

The root cause appears to be Nginx's expires directive interfering with manual Cache-Control headers. When both are present, Nginx may generate its own Cache-Control header, overriding custom settings.

Here's the corrected configuration that ensures proper cache control:

location ~* \.(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
    # Disable default expires behavior
    expires off;
    
    # Custom cache headers
    add_header Cache-Control "public, max-age=86400, immutable";
    add_header Pragma public;
    add_header X-Asset "yes";
    
    # Performance optimizations
    access_log off;
    log_not_found off;
    
    # Enable sendfile for better performance
    sendfile on;
    tcp_nopush on;
}

1. Disable expires directive: This prevents Nginx from generating conflicting Cache-Control headers

2. Explicit Cache-Control: We manually set all required caching parameters

3. Added immutable: For static assets that never change, this prevents unnecessary revalidation

After making these changes and reloading Nginx (nginx -s reload), verify with curl:

curl -I https://www.example.com/icons/facebook.png

Should now show:

HTTP/1.1 200 OK
Server: nginx/1.11.2
Date: Tue, 04 Oct 2016 15:30:22 GMT
Content-Type: image/png
Content-Length: 416
Last-Modified: Mon, 03 Oct 2016 18:18:37 GMT
Connection: keep-alive
Cache-Control: public, max-age=86400, immutable
ETag: "57f2a0fd-1a0"
X-Asset: yes

For more granular control, consider these variations:

# Different cache durations per file type
location ~* \.(js|css)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}

location ~* \.(jpg|jpeg|png|gif|ico)$ {
    add_header Cache-Control "public, max-age=2592000";
}

# Versioned assets with infinite cache
location ~* \.(js|css|woff2?|ttf|otf|eot|svg)[a-zA-Z0-9=_\-]+\.(js|css|woff2?|ttf|otf|eot|svg)$ {
    add_header Cache-Control "public, max-age=31536000, immutable";
}
  • Not clearing browser cache when testing changes
  • Forgetting to reload Nginx after configuration changes
  • Setting too long cache durations for frequently updated assets
  • Overriding headers in other configuration blocks