s-maxage

Caching

A Cache-Control response directive that sets how long a shared cache — a CDN edge, a reverse proxy — may serve a response as fresh. For shared caches it overrides max-age and Expires, and it forbids serving stale without revalidation. Private caches such as browsers ignore it and use max-age.

10 min read Updated Aug 30, 2026

Full Explanation

s-maxage is a response directive of the Cache-Control header. The directive gives shared caches their own freshness lifetime, in seconds. RFC 9111 defines it in one sentence: "The s-maxage response directive indicates that, for a shared cache, the maximum age specified by this directive overrides the maximum age specified by either the max-age directive or the Expires header field." A shared cache is one "that stores responses for reuse by more than one user". Examples are a CDN edge, a reverse proxy, an ISP or corporate proxy. A private cache, such as a browser's, is "dedicated to a single user". It ignores the directive. That is the whole point of it: one response can carry two TTLs. One is long, for the edge, and the other is short, set with max-age, for the browser.

It is not a browser control. Browsers read max-age. In Cloudflare's words, they "ignore s-maxage". So you cannot use it to shorten a copy a visitor already holds. It is not an override of the storage rules either. no-store still forbids storing. And private still means a shared cache "MUST NOT store the response". What it does do, beyond setting the edge TTL, is count as explicit freshness information that makes a response storable by a shared cache. It also acts as one of only three directives that let a shared cache reuse a response to a request carrying an Authorization header. It is not a stale-serving TTL. It carries proxy-revalidate semantics. So once the edge copy is stale, the edge must validate with the origin before it may reuse it. And it is not universally obeyed. Akamai ignores origin Cache-Control until you switch honouring on, and Cloudflare's Edge Cache TTL rules override it. The shape you are aiming for is a long edge TTL, a short browser TTL, and a purge when you publish.

How it works

  1. Syntax. The value is delta-seconds. RFC 9111 defines delta-seconds as "a non-negative integer, representing time in seconds". It must be unquoted: "This directive uses the token form of the argument syntax: e.g., 's-maxage=10' not 's-maxage="10"'. A sender MUST NOT generate the quoted-string form." Cloudflare adds that floating-point values "are not valid and will be ignored, potentially causing cache bypass".
  2. The freshness lifetime is chosen by first match. RFC 9111 section 4.2.1 evaluates its rules in order: "If the cache is shared and the s-maxage response directive ... is present, use its value", otherwise max-age, otherwise Expires minus Date, otherwise a heuristic. The first rule applies only when the cache is shared. So a browser falls straight through to max-age. Section 5.3 adds that when s-maxage is present, "a shared cache recipient MUST ignore the Expires header field".
  3. Freshness is a comparison, not a countdown from arrival. The test is "response_is_fresh = (freshness_lifetime > current_age)". Current_age includes the Age the response already carried. An object that has sat at the edge for hours is handed downstream with that age attached. So the browser measures its max-age against it, rather than starting from zero.
  4. Going stale means revalidating, not serving stale. s-maxage "incorporates the semantics of the proxy-revalidate response directive ... for a shared cache. A shared cache MUST NOT reuse a stale response with s-maxage to satisfy another request until it has been successfully validated by the origin". Section 4.2.4 says the same from the other side: a cache "MUST NOT generate a stale response if it is prohibited by an explicit in-protocol directive", naming "an applicable s-maxage or proxy-revalidate response directive". Private caches are untouched by this. That is because proxy-revalidate "does not apply to private caches".
  5. It unlocks authenticated responses for shared caches. A shared cache "MUST NOT use a cached response to a request with an Authorization header field ... to satisfy any subsequent request" unless a response directive permits it. RFC 9111 section 3.5 names exactly three that do: must-revalidate, public, and s-maxage. Adding s-maxage to a per-user response therefore invites sharing it between users.

Why it matters for a CDN

A CDN edge is the textbook shared cache. A visitor's browser is the textbook private one. So s-maxage is the one standard directive that addresses the edge alone. Raising the edge TTL lifts the cache hit ratio and keeps requests off the origin. Keeping the browser TTL short makes a published change visible quickly. With max-age alone, you must trade one against the other.

The asymmetry that makes the split worth doing is control. A CDN cache is a managed cache. MDN notes that with standard directives alone, "there's no way to actively delete cache contents when content is updated on the server". But "a CDN that allows cache purging via an API or dashboard operation would allow for a more aggressive caching strategy". A copy already stored in a browser is different. Responses "will remain in the browser cache until max-age expires, unless the user manually performs a reload, force-reload, or clear-history action". Or you can send Clear-Site-Data: cache, which MDN notes "has no effect on intermediate caches". So put the long TTL where you have a delete button. Keep the copy you cannot delete short-lived.

Vendors document that split as the normal pattern. Cloudflare: "You can also simultaneously specify a Cloudflare Edge Cache TTL different than a Browser's Cache TTL respectively via the s-maxage and max-age Cache-Control headers." Fastly's own worked example for caching at the edge but not in browsers is Cache-Control: s-maxage=3600, max-age=0.

What CDNs do

  • Cloudflare honours it through Origin Cache Control, which "Free, Pro and Business customers have ... enabled by default" and, per the same page, "cannot disable". Enterprise customers choose per zone through cache rules. With the feature on, an s-maxage above 1 behaves as "Max-age and proxy-revalidate", and s-maxage=0 behaves as "Caches and always revalidates". With the feature off, that same s-maxage=0 means "Will not cache". Edge Cache TTL cache rules "override s-maxage and disable revalidation directives if present".
  • Fastly reads it, but not first. Its documented order of preference for cache TTL is Surrogate-Control: max-age, then Cache-Control: s-maxage, then Cache-Control: max-age, then Expires. That order holds because the first two "express a desired TTL for server-based caches (such as Fastly's readthrough cache)" and so "will be given preference over Cache-Control: max-age when calculating the initial value of the response object's TTL". Fastly strips Surrogate-Control on the way out. But it "does not, however, remove the s-maxage directive from any Cache-Control header". It also documents a divergence from RFC 9111: at Fastly "the existence of the Authorization header in the request does not prevent a response from being cached". So s-maxage is not what makes authenticated responses cacheable there.
  • Akamai ignores it out of the box: "By default, edge servers don’t honor these two response headers sent from the origin server." Once "Honor origin Cache-Control and Expires" is enabled, the edge network honours the Expires value plus the s-maxage, max-age, no-store and no-cache directives. When both ages are present, "Akamai caches the content using the s-maxage value, and sends both directives downstream". Also, "Clients will ignore the s-maxage directive entirely and will only cache content using the max-age directive value or the Expires value, if passed". Akamai also clamps what it tells the browser, sending "the smaller of the Cache-Control: max-age and/or Expires values received from the origin server and the remaining lifetime of the object in the edge server cache". So "the client max-age is always equal to or lower than the edge max-age".

Watch out for

  • It cancels stale-serving at the edge. Pairing it with stale-while-revalidate does not work. s-maxage implies proxy-revalidate. So RFC 9111 forbids the stale response. Cloudflare states it plainly: "Do not use s-maxage with stale-while-revalidate. The s-maxage directive implies proxy-revalidate, which prevents shared caches from serving stale content." Cloudflare says the same of stale-if-error. It "is ignored ... if an explicit in-protocol directive is passed", including "an applicable s-maxage or proxy-revalidate cache-response-directive". A header such as s-maxage=86400, stale-while-revalidate=3600 looks like background refresh. It delivers blocking revalidation instead. Here is the trap in full:
    Cache-Control: public, max-age=60, s-maxage=86400, stale-while-revalidate=3600
    
  • The browser window is smaller than it looks. Age counts against max-age. So max-age=60, s-maxage=86400 does not promise a visitor 60 seconds of freshness. RFC 9213 works the arithmetic through with a CDN response carrying Age: 1800 and Cache-Control: max-age=600: "From the CDN's perspective, this response is still fresh after being cached for 30 minutes, while from the standpoint of other caches, this response is already stale."
  • It reaches past the edge you control. s-maxage applies to every shared cache on the path. CDNs forward it. Akamai is explicit: "If there are any caching proxies between Akamai and the client, they should also cache the content using the s-maxage value." You cannot purge an ISP or corporate proxy. So a very long s-maxage is a promise you may not be able to retract.
  • It makes authenticated responses shareable. RFC 9111 section 3.5 counts s-maxage among the directives that permit reuse of a response to a request bearing Authorization. So adding it to a per-user response invites a shared cache to serve one user's copy to another. Cloudflare shows the mechanism in production: with Origin Cache Control enabled and an Authorization header present, "Content is cached only if must-revalidate, public, or s-maxage is also present". Check the cache key and the Vary header before using it on anything personalised.
  • Not honoured is not the same as not documented. Whether s-maxage reaches the cache at all depends on configuration: opt-in at Akamai, outranked by Surrogate-Control at Fastly, overridable by Edge Cache TTL rules at Cloudflare. Read the response you actually get, not the header you set.

Best practice

  • Use the pair, not one value: a long s-maxage for the edge, a short max-age for the browser, and a purge on publish. Tag related objects with a surrogate key so one purge call covers a release.
  • Keep the value an unquoted integer number of seconds. The quoted form is forbidden by RFC 9111. Decimals are dropped by at least one major CDN.
  • If you want the edge to refresh in the background, do not gate edge freshness with s-maxage. Express the edge TTL in a targeted field instead: RFC 9213's CDN-Cache-Control, or Fastly's Surrogate-Control. Leave stale-while-revalidate in Cache-Control.
  • Prefer a targeted field when the edge and downstream policies really differ. A cache that uses one "MUST ignore the Cache-Control and Expires header fields in that response". So the edge TTL stops leaking to proxies you do not control. s-maxage stays as the fallback for caches that have not implemented RFC 9213.
  • Do not reach for s-maxage on personalised or authenticated responses. If a response belongs to one user, mark it private. Let the browser cache it alone.
  • Verify at the edge rather than in the config. Request the object twice. Read Age, X-Cache, and the Cache-Control header that comes back. Confirm the edge revalidates instead of serving stale once the s-maxage window passes.

Examples

This is a practical header for a product page:

Cache-Control: public, max-age=60, s-maxage=3600

The browser cache ends after 1 minute. The CDN keeps the copy for 1 hour. When prices change, you purge the CDN, and browsers use the new copy within 60 seconds.

In nginx, set this per location block:

location /products/ {
    add_header Cache-Control "public, max-age=60, s-maxage=3600, stale-while-revalidate=600";
}

Frequently Asked Questions

A Cache-Control response directive that sets how long a shared cache — a CDN edge, a reverse proxy — may serve a response as fresh. For shared caches it overrides max-age and Expires, and it forbids serving stale without revalidation. Private caches such as browsers ignore it and use max-age.

This is a practical header for a product page:

Cache-Control: public, max-age=60, s-maxage=3600

The browser cache ends after 1 minute. The CDN keeps the copy for 1 hour. When prices change, you purge the CDN, and browsers use the new copy within 60 seconds.

In nginx, set this per location block:

location /products/ {
    add_header Cache-Control "public, max-age=60, s-maxage=3600, stale-while-revalidate=600";
}

Related CDN concepts include:

  • max-age — max-age is a Cache-Control response directive giving the seconds a stored response may be reused …