HTTP Cache-Control Guide: Set, Test and Maintain Cache Headers

by Vytautas Gluoksnis, Performance Marketing & Web Engineering

Tips & tricks

Cache-Control failures usually come from applying one rule to every URL: HTML, account data, APIs, images and hashed JavaScript are treated as if they have the same privacy and update requirements. They do not. The fix is not choosing a fashionable max-age value; it is classifying each response, assigning directives that match that class, and checking the headers your production server actually returns.

Cache-Control carries directives in HTTP requests and responses that control caching in browsers and shared caches such as proxies and CDNs. A shared cache can reuse one stored response for multiple users, while a private cache is local to one client. That distinction should drive the first decision in your policy. MDN

1. Build an inventory of responses, not page templates

Begin with URLs that users and crawlers actually request. A single page can generate an HTML document, CSS, JavaScript, fonts, images and API responses, each needing a separate policy. Include authenticated routes and error-prone or operationally sensitive endpoints in the list.

For every response, record four fields: whether its content is identical for all users, whether it contains personal or sensitive data, how often it can change, and whether a changed file gets a new URL. Do not set headers until these fields are known.

  • Public HTML: landing pages, help pages, articles and category pages that show the same content to everyone.
  • Personalized or authenticated HTML/API: accounts, orders, inboxes, recommendations and checkout steps.
  • Versioned static files: build-generated JavaScript, CSS, fonts and images with a content hash or version in the URL.
  • Unversioned assets: files whose URL remains the same when their contents change.
  • Frequently changing shared data: availability, pricing, status and public feeds.

Inspect representative URLs from each group in your browser’s network panel or a request waterfall. DebugBear documents that its waterfall lets you open a resource’s Headers tab and see the Cache-Control response header, and provides a Cache Control column for comparing resources. DebugBear Record what is served now, including different results for logged-in and logged-out requests where your setup permits that test.

2. Protect personalized responses before optimizing them

For content intended for one user, start with private. It permits storage in a private cache such as a browser cache, but not in a shared cache. MDN specifically recommends private for personalized responses, especially after login and for cookie-managed sessions, because an omitted private directive can allow shared-cache reuse across users. MDN

Choose no-store where no cache should store the response at all. This applies to the response, not merely its display in a page. MDN states that no-store tells private and shared caches not to store the response. MDN It is a clear policy for responses where retaining a stored copy is unacceptable.

Cache-Control: private, no-cache

This combination is appropriate when a user-specific response may be kept in the browser but must be validated with the origin before reuse. The important correction is that no-cache does not mean “do not store”: it permits storage and requires validation with the origin server before each reuse. MDN

Do not use a browser back-button test as proof that no-cache or must-revalidate works. MDN notes that history navigation can restore a back/forward cache snapshot without revalidation. Verify a new network request or the relevant response headers instead. MDN

3. Set freshness separately for browsers and shared caches

Use max-age=N to state that a response remains fresh for N seconds after it was generated. While fresh, caches can store and reuse it. Freshness is measured from generation at the origin, not simply from the time the browser received it; an Age header from an intervening cache reduces the remaining lifetime. MDN

Set N from an explicit publishing rule: the longest period during which the old response is acceptable for that URL. Write that rule beside the header in your configuration or deployment documentation. This makes the setting reviewable when editorial, inventory or release processes change.

If shared caches need a different lifetime from browsers, add s-maxage. It applies to shared caches, is ignored by private caches, and overrides max-age for shared caches. MDN

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

This expresses a ten-minute browser freshness lifetime and a one-hour shared-cache lifetime. Use it only after confirming the response is safe to share. Public marks a response as storable in a shared cache; it is not a substitute for checking whether the response varies by person, session or authorization. MDN

4. Make versioned assets cache for a long time

For assets whose URL changes every time their contents change, use the cache-busting pattern: publish a new filename or URL rather than replacing the old content at the old URL. MDN describes this as using version or hash values in static-resource URLs and never modifying those resources. MDN

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

The exact duration is a policy choice; the example above uses one year. The essential safeguard is the changed URL. The immutable directive says that a response will not be updated while fresh and can avoid conditional revalidation on reload for such static resources. MDN recommends pairing immutable with a long max-age when using cache-busting URLs. MDN

Verification is simple: deploy a changed asset, confirm its referenced URL changed, then request both URLs. The old URL must still identify the old immutable file; the new page must reference the new URL. If a file changes without its URL changing, remove immutable and shorten freshness to fit the update process.

5. Decide explicitly what happens after freshness expires

For a shared response that must not be reused stale, pair its freshness lifetime with must-revalidate. Once stale, the cache must validate it with the origin before reuse; if it cannot, MDN says it must generate a 504 response rather than reuse stale content. MDN

Cache-Control: max-age=3600, must-revalidate

For content where a short stale window is acceptable, consider stale-while-revalidate. It allows a cache to reuse a stale response for the stated period while revalidating in the background. For example, max-age=604800 with stale-while-revalidate=86400 permits seven days of freshness followed by a one-day stale-while-revalidating window. MDN Document the accepted staleness in plain language; do not apply this to data where an old value is unacceptable.

Similarly, stale-if-error allows reuse of stale content when an upstream or local error is a 500, 502, 503 or 504 response. MDN Treat it as an availability decision and test it in a controlled environment before relying on it for a critical route.

6. Run a production header check after each release

Finish with a small regression checklist. Check the response itself, not just application code or CDN settings, because the reader needs to see the policy delivered to the client.

  • Open one live URL from every inventory class and record the exact Cache-Control value returned.
  • Confirm account and session pages return private or no-store according to the data policy.
  • Confirm public shared responses do not contain one user’s data when tested with separate user sessions.
  • Confirm hashed assets use a new URL after a content change before retaining immutable.
  • Confirm max-age and s-maxage values match the documented browser and shared-cache lifetimes.
  • After a release, compare the recorded headers with the intended configuration and investigate any missing or overridden directive.

Cache-Control supports multiple comma-separated directives, and directive names are case-insensitive, though lowercase is recommended because some implementations may not recognize uppercase directives. MDN Keep the final header strings short enough to audit, but make every directive answer a specific question: who may store this response, how long is it fresh, and what is allowed after it becomes stale.

Key takeaway

Start with an inventory of real response URLs. Assign headers by the data each response contains and how updates reach users, then inspect the headers served in production.

Sources

Share this

More articles

Want this measured on your own site?

Run a free GEO audit on any page — no account needed — or have the monitoring handled for you.