Configure an HTTP cache policy on your Nginx server to improve website performance. This policy instructs browsers and intermediate proxies, such as a Content Delivery Network (CDN), to cache static assets like images, CSS, and JS files. When assets are cached, browsers can load them directly from a local copy instead of requesting them from the server. This approach accelerates page load times, reduces bandwidth usage, and reduces server load.
Common cache policy examples
This section provides common caching configurations that you can add to your Nginx configuration file for different use cases. All examples use add_header ... always; to ensure cache headers are added to all responses, including 304 Not Modified.
Use case 1: Set long-term caching for static resources
# Set long-term caching for static resources
# - For filenames that contain 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 resources with hashes generated by build tools: cache for 1 year, browser never revalidates.
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 general static resources without hashes: cache for 30 days, allows CDN and browser caching.
add_header Cache-Control "public, max-age=2592000" always;
access_log off;
}Use case 2: Configure a cache policy for HTML documents or Single-Page Application (SPA) entry points
Do not set a long-term cache for HTML pages, especially the entry file of an SPA like index.html, because their content changes with new deployments. However, to balance performance, you can allow browsers to cache the file but 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.
# 'private' prevents intermediate proxies (like CDNs) 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 relies on the server returning an
ETagorLast-Modifiedresponse header, which enables the browser to make a conditional request. Nginx provides these headers for static files by default, so no extra configuration is needed.To prevent an intermediate proxy, such as a CDN, from caching HTML content, use the
privatedirective. This ensures only the end-user's browser can cache the response.
Use case 3: Disable caching for dynamic content or sensitive information
For dynamically generated content or content with sensitive data, such as API endpoints, user profile pages, or payment pages, you must disable caching. This applies to browsers and all intermediate proxies, such as CDNs and shared caches. Disabling the cache prevents information leaks and data inconsistencies.
# Example: For dynamic PHP scripts (adjust the path as needed)
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-side cache configuration (controlling the browser)
You can control the cache behavior of a user's browser by adding Cache-Control and Expires headers to the HTTP response. This practice reduces network requests and speeds up access for end users.
Core directives
expiresdirective: Sets both theExpiresheader and theCache-Controlheader'smax-age.Syntax:
expires [time|epoch|max|off];Example:
expires 30d;caches for 30 days.expires -1;forces the client to revalidate the resource with the server before using the cached version. This is equivalent toCache-Control: no-cachebut still allows the cache to store the resource.Note: The
add_headerdirective provides more granular control and is the recommended configuration method.
add_headerdirective: Adds a specified HTTP header to the response.Syntax:
add_header <name> <value> [always];alwaysparameter description
- By default,add_headerapplies only to 2xx and 3xx responses. - For304 Not Modifiedresponses, Nginx does not automatically add custom headers. Although the browser uses the cache policy from the initial 200 response, you should add thealwaysparameter to the cache control header. This ensures clarity and compatibility by applying the header to all response status codes.
Cache-Control key value descriptions
public: The response can be cached by any cache, including browsers, CDNs, and proxy servers.private: Only the end user's browser can cache the response. Shared caches, such as CDNs, are prohibited. This is suitable for content that contains user-specific information.no-cache: Requires the client to send a validation request to the server before each use of a cached copy. If the resource has not changed, the server returns304 Not Modified, and the client uses the local cache. This saves 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 cache validity period in seconds.immutable: Informs the browser that the resource's content will not change during its freshness lifetime. The browser can then skip the validation request for this resource, even when the user performs a full page refresh. This is ideal for files with version hashes in their names.
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 headers
Usecurlto check if the cache headers are active. This method is not affected by the browser cache.curl -I http://your-domain.com/path/to/file.jsThe output should include the expected headers, such as
Cache-Control: public, max-age=31536000.Verify the 304 behavior (if ETag or no-cache is configured)
Test by manually sending 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 # Expect a 304 responseVerify in the browser
Go to Developer Tools → Network panel.
Check the Disable cache box to see the initial load (should be a
200 OKstatus).Clear the checkbox and refresh the page:
No request or
from cacheis displayed. This indicates a strong cache hit.304is displayed. This indicates a conditional cache hit.
FAQ
Why aren't my Nginx Cache-Control changes taking effect after reloading the configuration?
This typically happens for one of three reasons: an outdated response is being served from a cache, Nginx hasn't actually reloaded the new configuration (sudo nginx -s reload), or a different location block is taking precedence.
To troubleshoot this, follow these steps:
Verify the server's live response. Use
curlto bypass any browser, CDN, or proxy caches and inspect the headers sent directly from your server.curl -I http://your-urlThis shows you which
Cache-Controlheader the server is actually sending.Confirm the Nginx configuration was reloaded successfully. After making changes, you must run
sudo nginx -s reloadfor them to apply.Check
locationblock matching priority. Nginx processeslocationblocks in a specific order. A request might be matching a more generic rule unexpectedly. Remember that regular expression matches (like~* \.(css|js)$) have a higher priority than prefix matches (likelocation /static/). A request for/static/app.jscould be incorrectly handled by the regex rule if it appears in your configuration.
How do I stop Nginx from caching dynamic API endpoints matched by a static asset rule?
This happens when a broad regular expression for static assets, like ~* \.js$, also matches a dynamic API path, such as /api/user.js.
To fix this, make your location rules more specific.
Restrict the path:
location ~* ^/static/.*\.(css|js)$Ensure that the
locationblock for dynamic interfaces, such as/api/or\.php$, has a higher match priority or explicitly excludes caching.
What is the correct way to set different Cache-Control policies for different file types without location block conflicts?
Using multiple location blocks to set cache policies for different file types can lead to conflicts due to Nginx's location matching priority. A cleaner, more robust solution is to use a map to define your caching logic based on the response's Content-Type.
Solution: Merge the policies. You can use a map to dynamically set the cache 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;