By configuring an HTTP cache policy for your Nginx server, you can instruct browsers and intermediate proxies, such as a CDN, to cache static assets like images, CSS, and JS files. Within their validity period, cached assets are loaded directly from a local copy, eliminating the need for a server request. This improves website loading speed, reduces server bandwidth, and lowers server load.
Common cache policy examples
This section provides common caching configurations that you can apply directly to your Nginx configuration file. All examples use the add_header ... always; directive to add a cache header to all responses, including those with a 304 Not Modified response status code.
Use case 1: Long-term cache for static assets
# Configure long-term caching for static assets
# - For filenames with a content hash (e.g., main.a1b2c3d4.js), a 1-year cache with 'immutable' is recommended.
location ~* "\.[a-f0-9]{8,}\.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2)$" {
# For assets with a hash generated by build tools: cache for 1 year, and instruct the browser to never revalidate.
add_header Cache-Control "public, max-age=31536003, immutable" always;
access_log off;
}
# - For filenames that are fixed and rarely change (e.g., logo.png), use a 30-day cache.
location ~* \.(css|js|png|jpg|jpeg|gif|svg|webp|ico|woff|woff2)$ {
# For generic static assets without a hash: cache for 30 days and allow both CDN and browser caching.
add_header Cache-Control "public, max-age=2592000" always;
access_log off;
}Use case 2: Cache for HTML and SPA
HTML pages, especially the entry file for a single-page application (SPA) like index.html, are updated frequently with each new deployment. Therefore, avoid long-term caching for them. For performance, you can allow browsers to cache these files but should require them to revalidate with the server before each use.
# For directly requested HTML files
location ~* \.html$ {
# Allows browser caching but requires revalidation with the server before each use.
# The 'private' directive prevents an intermediate proxy (like a CDN) from caching this response.
# The server must provide an ETag or Last-Modified header to support validation.
add_header Cache-Control "private, no-cache, must-revalidate" always;
}This policy requires the server to return an
ETagorLast-Modifiedheader, which allows the browser to make a conditional request. Nginx provides these headers for static files by default, so no extra configuration is needed.To prevent intermediate proxies, such as CDNs, from caching HTML content, use the "private" directive. This ensures that only the end user's browser caches the response.
Use case 3: No cache for dynamic and sensitive content
For dynamically generated or sensitive content, such as an API endpoint, user profile page, or payment page, prevent browsers and intermediate proxies (like CDNs or shared caches) from storing the response. This prevents data leaks and inconsistencies.
# Example: For dynamic PHP scripts (adjust the path based on your setup)
location ~ \.php$ {
# ... other PHP-FPM configurations ...
# Disable all caching: Browsers, proxies, and CDNs must not store the response.
# 'no-store' is the strictest cache control directive.
add_header Cache-Control "no-store" always;
}Client cache configuration
You can control browser caching by adding Cache-Control and Expires headers to HTTP responses.
Core directives
expiresdirective: Sets both theExpiresheader and theCache-Controlheader'smax-agevalue.Syntax:
expires [time|epoch|max|off];Example:
expires 30d;(caches the resource for 30 days),expires -1;(forces the client to validate the resource with the server before use (equivalent toCache-Control: no-cache), but allows the resource to be cached).Note: The
add_headerdirective is recommended because it offers more granular control.
add_headerdirective: Adds a specified HTTP header to the response.Syntax:
add_header <name> <value> [always];Description of the
alwaysparameter
- By default,add_headerapplies only to 2xx and 3xx responses. - For304 Not Modifiedresponses, Nginx does not automatically add custom headers. Although browsers will reuse the caching policy from the initial 200 response, you should add thealwaysparameter to cache control headers for clarity and compatibility. This ensures that the headers apply to all response status codes.
Cache-Control directives
public: The response can be stored by any cache, including browsers, CDNs, and proxy servers.private: The response can only be stored by the end user's browser, not by shared caches like CDNs. This is useful for content containing user-specific information.no-cache: Requires the client to send a request to the server to validate a cached copy before each use. If the resource has not changed, the server returns304 Not Modified, allowing the client to use the local cache and save bandwidth.no-store: Prohibits browsers and proxy servers from storing any part of the response. This is suitable for highly sensitive data.max-age=<seconds>: Sets the time-to-live (TTL) of the cache in seconds.immutable: Informs the browser that the asset's content will not change while cached. It is ideal for files that include a content hash in the filename.
Deploy and verify
Edit the configuration
Add thelocationblock to your site'sserverblock. The configuration file is typically located in/etc/nginx/conf.d/or/etc/nginx/sites-enabled/.Reload the configuration
sudo nginx -t && sudo nginx -s reloadVerify the response header
Usecurlto check for the cache header. This method bypasses any browser cache.curl -I http://your-domain.com/path/to/file.jsIt should include expected headers such as
Cache-Control: public, max-age=31536000.Verify
304behavior (forno-cacheorETag-based policies)
Test with a validation header:ETAG=$(curl -I http://example.com/file.js 2>/dev/null | grep -i etag | cut -d' ' -f2 | tr -d '\r') curl -H "If-None-Match: $ETAG" -I http://example.com/file.js # Expects a 304 responseVerify in a browser
Open Developer Tools and go to the Network panel.
Select the Disable cache checkbox to view the initial load. The status should be
200 OK.Clear the checkbox and refresh the page:
No request is made or is displayed
(from cache)→ strong cache hitReturns
304→ conditional cache hit
FAQ
Changes not taking effect
Cause:
The
sudo nginx -s reloadcommand was not executed.A browser, CDN, or proxy server is serving a stale response.
Incorrect
locationblock matching priority.
Solution:
Use
curl -I http://your-urlto verify the response header directly from the server.Check the
locationblock order. Regular expression matches (like~* \.(css|js)$) take precedence over prefix matches (like/static/). A request might be handled by the wrong rule because of this priority.
Dynamic content incorrectly cached
Cause: The regular expression for static assets is too broad (for example, ~* \.js$ matches /api/user.js).
Solution:
Restrict the path:
location ~* ^/static/.*\.(css|js)$Ensure the
locationfor dynamic interfaces such as/api/and\.php$is matched with priority or explicitly excluded from caching.
Conflicting cache policies
Cause: Multiple
locationblocks match the same request, but only the first one takes effect.Solution: Use a
mapblock to consolidate policies and dynamically set the cache policy based on the content type.# In the http block map $sent_http_content_type $cache_control { ~^image/ "public, max-age=2592000"; text/css "public, max-age=2592000"; application/javascript "public, max-age=2592000"; default "no-cache"; } # In the server block add_header Cache-Control $cache_control always;