Guides

Performance guide

How to Configure Cache-Control Without Breaking Your Website

Choose directives by audience and freshness requirements, then verify the effective policy in browsers and shared caches.

To configure Cache-Control safely, decide whether a response is public or private, whether it may be stored, how long it stays fresh, and whether stale content is acceptable. Then emit a small, explicit policy from the origin and check whether your CDN preserves or overrides it. The most important distinction is that no-cache allows storage but requires revalidation, while no-store tells caches not to store the response.

Cache-Control is an HTTP response header interpreted by browsers and shared caches such as CDNs and proxies. Cloudflare, Fastly, and equivalent providers may add an edge TTL, cache rule, or surrogate policy, so the header at the origin is the starting policy, not necessarily the final behavior.

The directives you need most

public, private, and no-store

public says a response may be stored by shared caches. private limits storage to a private cache, such as the user's browser, and is appropriate for personalized responses. no-store asks caches of any kind not to store the response. Use it for sensitive data, credentials, payment steps, or responses whose retention is unacceptable.

max-age and s-maxage

max-age gives the freshness lifetime in seconds for a response. s-maxage applies to shared caches and overrides max-age there, while browsers ignore it. Together they let you give a browser and a CDN different freshness windows:

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

Use integer seconds. Keep the window short when content changes often, and use versioned URLs for content that is genuinely immutable.

no-cache and validators

no-cache means that a stored response must be validated before reuse. It does not mean "do not cache." Pair it with ETag or Last-Modified so the validator can produce an efficient 304 Not Modified response:

Cache-Control: public, no-cache
ETag: "homepage-release-42"

This pattern is useful for HTML that should be checked on each navigation but does not need to be downloaded in full when unchanged.

immutable

immutable tells clients that a fresh response will not change. Use it only for resources whose URL changes when their bytes change:

Cache-Control: public, max-age=31536000, immutable

Never combine a long immutable lifetime with a mutable filename such as /styles.css unless you have a reliable versioning and purge strategy that prevents old references from remaining in circulation.

stale-while-revalidate and stale-if-error

stale-while-revalidate allows a cache to serve a stale response for a limited window while it revalidates in the background. stale-if-error allows stale content when the origin fails. They can improve resilience, but they extend the period in which old data may be seen. Use them only for content where that tradeoff is explicit.

Stale is a product decision

Serving an old article may be harmless; serving an old price, stock level, permissions response, or account balance may not be. Set stale windows per content class, not globally.

Safe policy patterns

Fingerprinted static assets

For hashed JavaScript, CSS, fonts, and media:

Cache-Control: public, max-age=31536000, immutable

The build must publish a new URL when the content changes. If a deployment keeps the same URL, choose a shorter lifetime and a tested purge process instead.

Public HTML that changes regularly

For public HTML that can be briefly fresh at the edge but should update in browsers more often:

Cache-Control: public, max-age=60, stale-while-revalidate=30
ETag: "page-2026-07-14-8"

This is a starting example, not a universal TTL. Check the actual publish frequency, user expectations, and the provider's stale semantics. If the shared cache needs a different freshness lifetime, use s-maxage as a separate policy and verify its interaction with stale directives. A CDN rule that forces a different edge TTL can override the origin's intent.

Public API data

For a read-only response that is identical for every anonymous request:

Cache-Control: public, max-age=30, s-maxage=120
ETag: "products-987"

Include the parameters that change the result in the cache key. Do not strip a query parameter merely because it looks optional. Verify pagination, filters, locale, currency, and authorization behavior.

Personalized or sensitive responses

For account or checkout data:

Cache-Control: private, no-store

If a non-sensitive personalized response can be retained by the browser but must not be reused by a shared CDN, private, max-age=60 may be appropriate after security review. Do not use a public cache rule to compensate for an ambiguous application response.

Headers that affect correctness

Vary tells caches which request headers select a different representation. If a response changes by language or encoding, make that variation explicit and confirm that the CDN honors it. Varying on a high-cardinality header, especially the whole Cookie value, can destroy hit ratio. Omitting a required variation can serve the wrong language, format, or device version.

Set-Cookie and Authorization require special attention. A session cookie often indicates personalization. A request with Authorization should not be served a shared response unless you have a deliberate, secure policy and the cache implementation follows it. If in doubt, use private or no-store and redesign the public/private boundary.

ETag and Last-Modified support conditional requests. Keep them stable for the same representation and change them when the representation changes. Content-Encoding and content negotiation must match the representation delivered to the client; test compressed and uncompressed paths.

Configure the edge without losing the origin contract

Treat the origin response as the source policy and the CDN as an enforcement layer. If Cloudflare, Fastly, or another provider uses an edge cache TTL, a cache rule, Surrogate-Control, or a provider-specific header, document its precedence. A setting that forces caching can defeat private or no-store; a setting that forces bypass can make a correct origin policy look ineffective.

Inspect both the origin and public responses. Compare a direct origin request with a request through the CDN, then test a cache miss, hit, revalidation, purge, and origin error. Never rely on a dashboard label alone.

Mistakes that break websites

Do not use no-cache when the requirement is no storage. Do not use no-store everywhere and then wonder why repeat navigations and browser back-forward behavior are slow. Do not cache HTML without checking cookies and authorization. Do not add immutable to a mutable URL. Do not set s-maxage without considering how it interacts with stale behavior at your provider. Do not forget that a purge cannot reach a browser cache that still considers an object fresh.

Validation checklist

For each response class, document its audience, cacheability, browser TTL, shared-cache TTL, validator, cache key, stale policy, and purge owner. Use curl -I and browser DevTools to inspect Cache-Control, Age, ETag, Last-Modified, Vary, Set-Cookie, status, and cache status. Test anonymous, authenticated, localized, and query-parameter variants. Confirm a changed response becomes visible within the promised window and that an origin error cannot expose sensitive or dangerously stale data.

For the wider cache design, read CDN caching explained. For reducing the origin delay behind a miss, see How to reduce TTFB, managed CDN delivery, and managed DNS.

There are only two hard things in Computer Science: cache invalidation and naming things.

Review your caching policy before it becomes an incident

Talk to our team about aligning origin headers, CDN rules, freshness, and invalidation with your application behavior.

Contact Us