Six Directives and What They Actually Mean

HTTP caching is specified in RFC 9111, where each directive has a precise definition. The trouble is that several are named against intuition, and most people copy them from someone else's configuration without reading the spec.

The key distinction is between storing and using: a cache may hold a response but might need to validate it before serving. no-cache governs the second, no-store the first — easily confused by name, entirely different in behaviour.

Directive Actual meaning Common misreading
no-cache May be stored, but must be revalidated before each reuse Read as "don't cache" — it isn't
no-store Must not be stored at all, headers included This is the one that means "don't cache"
max-age=N Fresh for N seconds, applies to every cache Assumed sufficient for CDNs too
s-maxage=N Shared caches only; overrides max-age and Expires Not knowing it overrides max-age
must-revalidate Stale responses can't be served unvalidated; errors if offline Expecting stale content as a fallback
private Browser may store; shared caches must not Conflated with no-store, so used too defensively
Six common Cache-Control directives and what RFC 9111 actually says they mean.

The table covers the six most-used directives. The first is the most misunderstood of them.

Three Real Consequences of the Confusion

The naming problem isn't academic. All three of these happen:

  1. Using no-cache to protect sensitive data doesn't work Wanting billing pages or personal details to leave no trace, people set no-cache and consider it handled. Per the specification, a cache may still write the entire response to disk — it just has to validate before reusing it. Browser caches on shared machines and corporate proxies can both retain that content. Genuinely preventing storage requires no-store.
  2. Applying no-store everywhere costs performance Having learned no-store is safer, some apply it site-wide. Every request then goes fully back to origin, the CDN becomes decorative, and static assets stop being reused by browsers. The right approach is identifying which responses genuinely contain personal data — usually only authenticated dynamic pages — and caching the rest normally.
  3. Not knowing s-maxage ties CDN and browser together max-age applies to browsers and CDNs alike, so one directive can't cache longer at the edge than in the browser. s-maxage exists for exactly that: it affects shared caches only and overrides max-age. Setting max-age=60, s-maxage=86400 means one minute in the browser and one day at the edge — the most basic CDN tuning tool, and among the most overlooked.

Vary: the Header That Sets the Cache Key

Caches key on the URL by default. But when one URL returns different content depending on request headers — compressed or uncompressed by Accept-Encoding, say — the cache has to know, or it will hand a compressed response to a client that can't read it. Vary declares which request headers affect the response.

Per the specification, a cache must not reuse a stored response unless every header named in Vary matches the original request. So the more Vary lists, the lower the hit rate — each header named subdivides the cache key again.

If your CDN hit rate is unreasonably low, Vary is the first thing to check. The problem produces no error at all — it just quietly sends every request to origin.

After Expiry: ETag and Conditional Requests

An expired cache entry doesn't mean the content changed. The cache can issue a conditional request, carrying the ETag it received (a content fingerprint) or Last-Modified, asking the origin whether this is still current. If nothing changed, the origin answers 304 Not Modified — headers only, no body.

This is what no-cache is genuinely for: it forces that validation every time while keeping the fast 304 path. For content that must be current but rarely changes — a homepage, typically — no-cache plus ETag is far more efficient than no-store retransmitting everything, while still guaranteeing users never see stale content.

Three Levels of Configuration

In practice, sort content into three categories:

1. Versioned or fingerprinted static assets

JS, CSS and images with a hash in the filename never change — a change produces a new filename. These take a long max-age plus immutable, so browsers skip validation entirely. Highest performance gain, lowest risk.

2. Changing but non-sensitive content

HTML pages and public API responses. Suited to no-cache or a short max-age with ETag — always current, but costing only a 304 when unchanged. Add s-maxage to keep it longer at the edge if you want the CDN absorbing most traffic.

3. Responses containing personal data

Authenticated pages, billing information, anything identity-bearing. These need no-store, not no-cache. If the intent is merely keeping it out of CDNs while browsers may store it, that's what private is for. Don't conflate the three.

Three Things to Check When Caching Misbehaves

When hit rates disappoint or content won't update, these are the usual causes:

  1. What Vary lists Inspect the actual response headers with developer tools or curl. If it names User-Agent or Cookie — headers with enormous value spaces — hits become nearly impossible, and nothing warns you.
  2. How s-maxage relates to max-age If CDN caching duration doesn't match the max-age you set, look for s-maxage — it overrides max-age and affects shared caches only. With both present, it's easy to misread.
  3. Which cache layer you're looking at Browser, CDN edge and reverse proxy each store separately. The Age response header reports how long the content has sat in a cache — more reliable than guessing which layer served it.

In Summary

no-cache permits storage and requires validation; no-store forbids storage. The naming confusion writes sensitive data to disk on one side while discarding the entire caching layer on the other. The costs differ, but the cause is the same.

What usually decides CDN effectiveness, though, isn't these directives but Vary and s-maxage — the two least discussed. If cross-border traffic depends on the CDN absorbing origin requests, those two matter more than node count.

Sources