Cache-Control is the response’s instruction to caches. It tells them whether reuse is allowed and how freshness should be handled. A broad rule such as “cache everything for a day” skips the question that matters first: who is allowed to receive this response?

Decide who can store the response

Use private when the response may be stored by a private cache but must not be stored by a shared cache. Use no-store when caches must not store the request or response under the directive’s rules. Neither directive erases data already stored elsewhere or replaces an access-control check.

The public directive explicitly permits shared caching in situations that might otherwise prevent it. Apply it only when sharing is safe. A response that contains account data is not made safe by a short TTL.

Storage and reuse are different decisions
DirectiveMeaningCommon mistake
privateShared caches must not store the responseAssuming it disables browser storage
no-storeCaches must not store the responseTreating it as a purge of older copies
no-cacheValidate before reuseAssuming storage is forbidden
publicExplicitly allows shared cachingUsing it for personalized HTML

Give browser and shared caches separate lifetimes

max-age specifies a freshness lifetime in seconds. For shared caches, s-maxage overrides max-age and Expires. This lets you keep a public page briefly at the edge while requiring the browser to validate its stored copy.

The example allows an eligible public response to remain fresh for five minutes in a shared cache. The browser’s freshness lifetime is zero, so a stored browser copy normally needs validation before reuse. A CDN configuration that overrides origin headers can change the effective policy.

Public page with a separate shared-cache lifetime
Cache-Control: public, max-age=0, s-maxage=300
ETag: "page-version-42"

# Shared cache: 300 seconds of freshness.
# Browser: stored copy is immediately stale.
# Confirm the CDN honors the origin policy.

Use validators when a stored response becomes stale

An ETag or Last-Modified value lets a cache ask whether the representation changed. A successful not-modified response can avoid transferring the full body, but validation still involves an upstream exchange unless another cache can answer it.

A validator does not invalidate fresh copies automatically. If a mutable URL has a long freshness lifetime, publishing a new ETag at origin does not force every cache to check immediately.

How it works

Freshness decides when validation is needed

  1. 01FreshReuse while policy allows it.
  2. 02StaleValidate or fetch according to policy.
  3. 03UpdatedStore the eligible new representation and lifetime.
Conceptual behavior for an eligible stored response. Request directives, cache rules, and extension directives can modify the decision.

Version immutable assets, invalidate mutable URLs

Content-hashed filenames give changed bytes a new URL. That supports long-lived caching while older pages continue to reference older assets. Do not overwrite a versioned filename with different content.

HTML, manifests, and other stable pointers need a separate update strategy. Use a suitable lifetime and a narrow purge when required. Remember that invalidating a CDN copy does not remove a fresh copy from a visitor’s browser.

Versioned public asset; apply only when its URL changes with its bytes
Cache-Control: public, max-age=31536000, immutable

# Example URL: /assets/app.8d31c2.js
# Publish changed bytes at a new content-hashed URL.

Test the policy from the client side

Inspect response headers from the public endpoint, then repeat the request and compare cache status, Age where present, and the response body. Change the origin resource in a controlled test and verify the freshness or invalidation path you intended.

Test cookie-bearing and authenticated requests separately. If a request can receive a different body because of a user, permission, query parameter, or representation header, that distinction must be reflected in the bypass or key policy.

Sources & further reading