Configure an HTTP cache policy on your NGINX origin server to control how browsers and CDN edge nodes store and serve your content, reducing origin load and improving page load times.
How cache policies work
When a browser or Alibaba Cloud CDN receives a response from your origin server, it checks the response headers to decide whether to cache the content, how long to keep it, and when to revalidate. By setting Cache-Control and Expires headers in NGINX, you give both browsers and CDN edge nodes explicit caching instructions instead of relying on default behavior.
NGINX provides two directives for this:
add_header: Adds a specific HTTP header to the response. Use this for fine-grainedCache-Controlpolicies.expires: Sets both theExpiresheader and themax-agevalue inCache-Controlusing a shorthand syntax.
The add_header directive offers more control and is the recommended approach for most use cases.
Prerequisites
Before you begin, ensure that you have:
NGINX installed and running as the origin server for your Alibaba Cloud CDN domain
Access to edit NGINX configuration files (typically
/etc/nginx/nginx.confor files under/etc/nginx/conf.d/)
The add_header directive
add_header <name> <value> [always];
By default, add_header applies only to 2xx and 3xx responses. Add the always parameter to apply the header to all response codes, including 4xx and 5xx. For cache control headers, always include always — this ensures the browser and CDN receive consistent cache instructions even when NGINX returns an error response.
Cache-Control directives reference
The following table lists the Cache-Control directives most commonly used with CDN configurations.
|
Directive |
Effect |
Typical use case |
|
|
Allows both browsers and shared caches (CDNs) to store the response |
Static assets served to all users |
|
|
Restricts caching to the user's browser only; CDNs must not cache |
Personalized or user-specific content |
|
|
Caches the response but requires revalidation before each use |
HTML pages, frequently updated resources |
|
|
Prevents any caching; response is never stored |
Sensitive data (login pages, payment responses) |
|
|
Sets how long (in seconds) the browser treats the response as fresh |
All cacheable resources |
|
|
Same as |
Resources where CDN TTL (time to live) should differ from browser TTL |
|
|
Tells the browser the resource will never change during its |
Versioned assets with content hashes in the filename |
|
|
After the |
Resources requiring strict freshness guarantees |
|
|
Serves a stale response immediately while fetching a fresh copy in the background |
Resources where some staleness is acceptable in exchange for faster responses |
The expires directive
expires [time|epoch|max|off];
The expires directive is a shorthand alternative. expires 30d; sets Cache-Control: max-age=2592000 and a matching Expires response header. expires -1; produces Cache-Control: no-cache, which tells the browser to revalidate before reuse while still allowing the response to be stored.
Use expires for simple time-based policies on static assets. Use add_header Cache-Control when combining multiple directives (for example, public, max-age=31536000, immutable).
Cache policy examples by content type
Different types of content require different cache strategies. The following examples show recommended NGINX configurations for four common scenarios.
Versioned static assets (with content hash in filename)
Assets like app.a1b2c3d4.js or style.8f3e1a2b.css contain a hash in the filename that changes whenever the file content changes. Cache these aggressively — the URL itself signals that the content is unique.
location ~* "\.[a-f0-9]{8,}\.(css|js|png|jpg|gif|svg|woff2)$" {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
max-age=31536000: Cache for one year (the practical maximum).immutable: Tells the browser to skip revalidation during themax-agewindow. Withoutimmutable, some browsers send a conditional request on reload even when the cache is fresh.public: Allows CDN edge nodes to cache and serve the asset.
Non-versioned static assets (fixed filename)
Assets like logo.png or favicon.ico use a fixed filename. Cache them for a shorter period to allow updates to propagate within a reasonable time frame.


location ~* \.(css|js|png|jpg|gif|svg|ico|woff2)$ {
add_header Cache-Control "public, max-age=2592000, must-revalidate" always;
}
max-age=2592000: Cache for 30 days.must-revalidate: After 30 days, the cache must revalidate before serving the asset again.Omit
immutablebecause the file content may change without a filename change.
HTML pages and SPA entry points
Never cache HTML pages for long periods — they reference versioned asset URLs that change with each deployment. If the HTML is stale, users load outdated asset references.
location ~* \.html$ {
add_header Cache-Control "private, no-cache, must-revalidate" always;
}
private: Prevents CDN nodes from caching the HTML. The CDN fetches it directly from the origin on every request.no-cache: Allows the browser to store the response but requires revalidation before each use. If the server returns304 Not Modified, the browser serves its cached copy without re-downloading the file.NGINX provides
ETagandLast-Modifiedheaders for static files by default, so no additional configuration is needed to support conditional requests.
Dynamic content (PHP, API responses)
For dynamic content that changes per request — such as user-specific pages or PHP-generated responses — prevent CDN caching while still allowing browsers to revalidate.
location ~ .*\.php$ {
if ($request_uri !~ ^/dynamicimg/) {
add_header Cache-Control "no-cache";
add_header Pragma no-cache;
}
}
The Pragma: no-cache header is an HTTP/1.0 legacy header included for backward compatibility with older clients and proxies. Modern clients use Cache-Control instead.
For responses containing sensitive data (for example, payment confirmations or authentication tokens), use no-store to prevent any caching:
location /api/user {
add_header Cache-Control "private, no-store" always;
}
Separate CDN and browser cache durations with s-maxage
By default, max-age controls both browser and CDN TTL (time to live). Use s-maxage when you want CDN edge nodes to cache content longer than browsers do — for example, to reduce origin load without over-caching in the browser.
location ~* \.(jpg|png|gif|svg)$ {
# Browsers cache for 1 hour; CDN edge nodes cache for 24 hours.
add_header Cache-Control "public, max-age=3600, s-maxage=86400" always;
}
max-age=3600: Browser TTL is 1 hour.s-maxage=86400: CDN TTL is 24 hours. CDN nodes ignoremax-agewhens-maxageis present.
This combination works well for large images and public API responses where a longer CDN TTL reduces origin traffic while keeping browser caches reasonably fresh.
Verify your configuration
After updating your NGINX configuration, verify that the headers are applied correctly.
Test the NGINX configuration syntax:
sudo nginx -t
Reload NGINX to apply changes:
sudo nginx -s reload
Send a request and inspect the response headers:
curl -I https://<your-domain>/assets/app.a1b2c3d4.js
Expected output for a versioned asset:
HTTP/1.1 200 OK
Cache-Control: public, max-age=31536000, immutable
Verify that Alibaba Cloud CDN is forwarding the headers from the origin. In the CDN console, confirm that no cache TTL override rules conflict with your NGINX headers.

