max-age
max-age is a Cache-Control response directive giving the seconds a stored response may be reused before it is stale, counted from when the origin generated it. It binds every cache in the chain — browser, CDN, reverse proxy — and permits reuse; it is not an eviction timer.
Full Explanation
max-age is a directive of the Cache-Control response header. Its value is the freshness lifetime the origin grants a response, counted in seconds. This is how long any cache in the chain may reuse a stored copy before that copy is stale. The chain can include a browser, a CDN, or a reverse proxy (RFC 9111 §5.2.2.1). It is not a TTL in the eviction sense. It governs whether a stored copy may be reused, not whether it is still stored. Many caches will evict a response far sooner than a long max-age suggests (RFC 9111 §5.3). It is not the absolute date carried by the Expires header either. When max-age is present, a recipient must ignore Expires (RFC 9111 §5.3). And it promises nothing about the content. An expiration time expresses the origin’s belief that the representation is not likely to change in a semantically significant way before then. It is not a prediction that it will change afterwards (RFC 9111 §4.2). The same token sent by a client in a request means something different again.
How it works
The response directive takes an unquoted integer number of seconds. A cache considers the response stale once its age is greater than that number (RFC 9111 §5.2.2.1). The clock does not start when a cache stores the copy. A response’s freshness lifetime is the time between its generation by the origin server and its expiration time. The only test a cache applies is whether the freshness lifetime still exceeds the current age (RFC 9111 §4.2).
The life cycle of one cached response:
- The origin sends Cache-Control: max-age=N. Each cache then picks a freshness lifetime by the first rule that matches:
s-maxageif the cache is shared, otherwisemax-age, otherwise Expires minus Date, otherwise no explicit expiration time is present at all (RFC 9111 §4.2.1). - While the stored response is fresh, any cache on the path can use it to satisfy subsequent requests without contacting the origin server (RFC 9111 §4.2). That single property is the entire economy of edge delivery.
- Elapsed time travels with the response in the Age header. This header holds a cache’s estimate of the seconds since the origin generated or validated the response. That estimate is the sum of the time it has been resident in each cache along the path plus the time in transit (RFC 9111 §4.2.3).
Age, not local wall-clock time, is what spends the lifetime. - Once age exceeds N the copy is stale. A cache that wants to keep using it revalidates. It sends a conditional request carrying
If-None-Matchfrom a stored ETag, orIf-Modified-Sincefrom a stored Last-Modified timestamp (RFC 9111 §4.3, §4.3.1). If nothing changed, the origin answers 304 Not Modified. The cache then freshens the stored response with the new information instead of transferring the body again (§4.3.4). - Send neither
max-agenor Expires, and the choice stops being yours. A cache MAY assign a heuristic expiration time, using other field values such as Last-Modified to estimate a plausible one. Sendingmax-ageforecloses that guess, because a cache MUST NOT use heuristics when an explicit expiration time is present in the stored response (RFC 9111 §4.2.2).
Two companion directives change the answer. s-maxage overrides the maximum age set by max-age or by Expires, but only for a shared cache: a CDN or a proxy. This is how one response can carry a short browser lifetime and a long edge lifetime at once (RFC 9111 §5.2.2.10). And max-age silences Expires. A recipient that implements Cache-Control MUST ignore the Expires field when max-age is present. So the absolute date only ever reaches recipients that never implemented Cache-Control (RFC 9111 §5.3).
Why it matters for a CDN
max-age is the number that decides how many requests a CDN answers by itself and how many it forwards, so it sits directly beneath the cache hit ratio and the request load that reaches the origin. While the stored response is fresh, the edge satisfies the request without contacting the origin server at all (RFC 9111 §4.2). This collapses a cross-network round trip into one short hop. Set it too high, and a change you have already published keeps missing every user holding a fresh copy, because expiry is the only event that returns them to the origin. Set it too low, and the edge revalidates constantly. That hands back the latency and the origin bandwidth the CDN was deployed to save. The lifetime is spent by accumulated Age, not by time at your edge. So a value tuned for a single hop behaves differently the moment a tiered cache or a customer proxy sits in the path (RFC 9111 §4.2.3).
What CDNs do
max-age is standardised, so a conforming CDN honours it. What differs is when the origin’s number wins, what the edge does with an invalid one, and what it substitutes when the origin sends nothing. Confirm the behaviour in each provider’s current documentation:
- Cloudflare follows origin Cache-Control through its Origin Cache Control feature. With the feature enabled, directives in the origin response are followed as specified. So a
max-ageof 3,600 seconds caches the resource for that duration before Cloudflare checks the origin again. Free, Pro and Business zones have it enabled by default and cannot disable it. Enterprise zones choose, through cache rules or the API. Where a Browser Cache TTL is also configured, Cloudflare respects whichever value is higher, that setting or the origin’smax-age. Browser Cache TTL is the browser-facing lifetime. It is distinct from Edge Cache TTL, which is the setting that pairs withs-maxage. Floating-point values such asmax-age=2.5are not valid. They are ignored, and can cause a cache bypass (Cloudflare docs). - Fastly derives an object’s cache TTL from the first of these headers present, in order:
Surrogate-Control: max-age, thenCache-Control: s-maxage, thenCache-Control: max-age, then Expires. A cacheable response carrying none of them is not left uncached. It gets a documented default TTL of 2 minutes. Fastly also subtracts a positiveAgealready present on the response from thatmax-age, treating a negative result as zero. So an upstream cache that has already spent your lifetime shortens Fastly’s (Fastly docs).
Watch out for
- In a request the same name is a different instruction. A client’s
max-agesays it prefers a response whose age is less than or equal to N seconds. Unless max-stale is also present, it also says the client does not wish to receive a stale response (RFC 9111 §5.2.1.1). But request directives are advisory. Caches MAY implement them and are not required to (§5.2.1). A client cannot dictate your edge lifetime. - An invalid value is worse than no value. The argument is delta-seconds, a non-negative integer. A sender MUST NOT generate the quoted form:
max-age=5, notmax-age="5"(RFC 9111 §5.2.2.1, §1.2.2). Caches are encouraged to treat a response with invalid freshness information, such as amax-agedirective with non-integer content, as stale (RFC 9111 §4.2.1). So one typo converts a year of caching into none. - Very large numbers saturate. A cache that receives a delta-seconds value greater than it can represent MUST treat it as 2147483648. That is over 68 years, which the specification uses to represent infinity (§1.2.2). Historically a year was the ceiling. Extremely large values have been demonstrated to cause problems such as clock overflow (RFC 9111 §5.3). Nothing is gained above one year.
- Stale is not deleted, and stale is not servable by default. Expiry does not remove the copy. It does not authorise serving it either. A cache MUST NOT generate a stale response unless it is disconnected, or unless doing so is explicitly permitted. Permission can come from max-stale on the request, from extension directives such as stale-while-revalidate and stale-if-error, or from an out-of-band contract (§4.2.4).
- It cannot make a browser reload. Freshness applies only to cache operation. It cannot be used to force a user agent to refresh its display or reload a resource (RFC 9111 §4.2). A history mechanism such as the Back button can redisplay a previous representation even after it has expired (§6).
- s-maxage forbids stale serving at a shared cache.
s-maxageincorporates the semantics of proxy-revalidate. A shared cache MUST NOT reuse a stale response carryings-maxageuntil the origin has successfully validated it (RFC 9111 §5.2.2.10). An applicables-maxageis also listed among the directives that prohibit generating a stale response at all (§4.2.4). If you are counting on stale serving at the edge, check your provider’s documentation before adding it.
Best practice
- Derive the seconds from the content class, not from habit. Use short lifetimes for HTML and for API responses that genuinely change. Use a long one for assets whose URL carries a version or a content hash, since such resources are never updated in place. They are republished under a new URL with references rewritten (RFC 8246 §1). One year, 31536000 seconds, is the conventional ceiling (RFC 9111 §5.3).
- Pair a long max-age with immutable on versioned assets. immutable indicates that the origin will not update the representation during the freshness lifetime. So clients SHOULD NOT issue a conditional request within it. This removes the wasted 304s a user-driven reload otherwise generates across every sub-resource (RFC 8246 §2). The RFC’s own example is exactly this pairing:
Cache-Control: max-age=31536000, immutable(§2.2). - Split browser and edge deliberately. A short
max-agekeeps the browser copy correctable. A longers-maxagelets the CDN absorb the traffic, because only a shared cache readss-maxage(RFC 9111 §5.2.2.10). You can purge an edge. You cannot purge a browser. - Always ship a validator. With an ETag or a Last-Modified value on the response, expiry costs one conditional request and a 304 that freshens the stored copy, rather than a full body transfer (§4.3.1, §4.3.4).
- Read Age on a real response before trusting your number.
Ageis a cache’s estimate of the seconds since the origin generated or validated the response (RFC 9111 §4.2.3). An unexpectedly high value tells you an upstream hop has already spent most of the lifetime you set. At Fastly, it is subtracted from themax-ageoutright (Fastly docs).
Here is how you would set it in nginx for different content types:
# Static assets: cache for 1 year
location ~* \.(css|js|png|jpg|webp|avif|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML pages: cache for 10 minutes
location / {
add_header Cache-Control "public, max-age=600";
}
# API responses: cache for 30 seconds
location /api/ {
add_header Cache-Control "public, max-age=30";
}
Examples
Files with a content hash in the name can be cached for a long time. The URL changes when the file changes:
Cache-Control: public, max-age=31536000, immutable
API responses that change often need a short max-age:
Cache-Control: public, max-age=30
A typical page sits in between. The browser keeps it for 10 minutes, and the CDN keeps it for an hour:
Cache-Control: public, max-age=600, s-maxage=3600
Frequently Asked Questions
max-age is a Cache-Control response directive giving the seconds a stored response may be reused before it is stale, counted from when the origin generated it. It binds every cache in the chain — browser, CDN, reverse proxy — and permits reuse; it is not an eviction timer.
Files with a content hash in the name can be cached for a long time. The URL changes when the file changes:
Cache-Control: public, max-age=31536000, immutable
API responses that change often need a short max-age:
Cache-Control: public, max-age=30
A typical page sits in between. The browser keeps it for 10 minutes, and the CDN keeps it for an hour:
Cache-Control: public, max-age=600, s-maxage=3600
Related CDN concepts include:
- s-maxage — A Cache-Control response directive that sets how long a shared cache — a CDN edge, …