Infrastructure & Networking

What is a cache-control header?

The Cache-Control header is an HTTP response, and sometimes request, header that provides explicit instructions to browsers, CDN edge servers, reverse proxies, and other caching intermediaries about how to store, serve, and validate cached copies of HTTP responses. It is the primary mechanism through which web servers and applications control caching behaviour across the entire chain of infrastructure between the origin server and the end user, specifying how long a response should be considered fresh, who is permitted to cache it, whether it must be revalidated before serving, and whether it should be stored at all.

Introduced in HTTP/1.1 Cache-Control replaced and superseded the older Expires header, which specified caching through an absolute date, with a more flexible directive-based system. Where Expires required the server to calculate and specify a future date for cache expiry Cache-Control specifies relative durations and behaviours, max-age=3600 means the response is fresh for 3600 seconds from the time it was received regardless of clock synchronisation between client and server. This relative approach is more reliable and more flexible than absolute date-based expiry.

Cache-Control headers appear in HTTP responses from servers, instructing downstream caches how to handle the response, and may also appear in HTTP requests from clients, instructing caches and proxies about the client’s caching preferences for that specific request. Response Cache-Control is the most operationally important, it is what server operators configure to control how their responses are cached throughout the infrastructure chain.

Response Cache-Control directives

Response Cache-Control headers consist of one or more directives, keywords that specify different aspects of caching behaviour. Multiple directives are combined with commas, Cache-Control: public, max-age=86400, must-revalidate.

max-age=seconds: the most widely used directive. Specifies the maximum time in seconds from the time of the response that the response is considered fresh. Cache-Control: max-age=3600 means the response is fresh for 3600 seconds, one hour. During this freshness window caches serve the stored response without contacting the origin server. After the freshness window expires the response is stale, caches must revalidate with the origin before serving.

Max-age is calculated from the Date response header, the time the response was generated. A response with max-age=3600 generated at 14:00 is fresh until 15:00 regardless of when different caches received it. If a CDN edge server cached the response at 14:00 and a browser received it from the CDN at 14:30 the browser’s cache expires at 15:00, 30 minutes after receipt, not 60 minutes.

s-maxage=seconds: shared cache max-age. Functions identically to max-age but applies specifically to shared caches, CDN edge servers, reverse proxies: rather than private browser caches. Cache-Control: max-age=600, s-maxage=86400 tells browsers to cache for 10 minutes while CDN edge servers cache for 24 hours. When s-maxage is present shared caches use s-maxage and ignore max-age for their cache duration calculation.

This two-tier caching strategy is particularly useful for pages where browser freshness requirements differ from CDN requirements. A product page might need browser cache to expire every 10 minutes so users see price updates quickly, while CDN caching for 24 hours reduces origin load significantly.

no-cache: frequently misunderstood, no-cache does not prevent caching. It instructs caches to store the response but to revalidate it with the origin before serving it to any subsequent request. The cached response is usable, but only after the origin confirms it is still current, through a conditional request using ETag or Last-Modified headers. If the origin returns 304 Not Modified the cache serves the stored response. If the origin returns a new 200 OK response the cache stores and serves the new response.

No-cache is appropriate for responses where freshness is critical, the cache provides efficiency through conditional requests, avoiding retransmitting the response body when content has not changed, while ensuring users never see content that the origin has updated. Compared to no-store no-cache is more efficient, no-store requires the full response body to be fetched every time.

no-store: genuinely prevents caching. Instructs all caches, browser and shared, not to store any part of the response. Every request for the resource fetches a fresh response from the origin, no copy is retained in any cache. Appropriate for responses containing sensitive personal data that must not persist in any cache, financial transaction confirmations, medical records, authentication tokens.

No-store has a performance cost, every request requires a full origin round trip with full response transmission. Use only when genuine data sensitivity requires it, not as a catch-all for dynamic content that could be handled more efficiently with no-cache.

public: explicitly marks the response as cacheable by shared caches even in contexts where shared caching would otherwise be prohibited. HTTP caching rules prohibit shared caching of responses to authenticated requests by default, a request with an Authorization header receives a response that shared caches will not store. Adding public overrides this default, explicitly authorising shared caching of the authenticated response. Used when authenticated API responses contain non-sensitive data that should be CDN-cached for performance.

private: marks the response as cacheable only in private caches, the user’s browser, not by shared caches. Cache-Control: private, max-age=3600 allows the browser to cache the response for one hour but instructs CDN edge servers and proxies not to cache it. Appropriate for personalised responses, pages containing the user’s name, account information, or personalised content, that should be cached for browser performance but not shared across users through CDN caches.

must-revalidate: once the cached response becomes stale, after max-age expires, must-revalidate requires the cache to successfully revalidate with the origin before serving the stale response. Without must-revalidate some caches may serve stale responses when the origin is temporarily unreachable, prioritising availability over freshness. With must-revalidate the cache must return an error if the origin cannot be reached for revalidation, prioritising freshness over availability.

Must-revalidate is appropriate for content where serving stale data is unacceptable, financial data, inventory counts, time-sensitive information. It ensures caches never serve outdated content even when serving stale content might have been possible.

proxy-revalidate: like must-revalidate but applies only to shared caches, CDN edge servers and proxies. Private browser caches are not bound by proxy-revalidate. Allows browser caches to serve stale content when the origin is unreachable while requiring shared caches to revalidate or return errors.

immutable: signals that the response body will not change during its freshness lifetime. Introduced to prevent browsers from making conditional revalidation requests on user-initiated reloads, a behaviour that normally bypasses browser cache freshness. With immutable browsers skip revalidation even on manual reload, trusting that the cached response is definitively current for the entire max-age duration.

Cache-Control: max-age=31536000, immutable is the canonical pattern for versioned static assets. The file content is immutable for the URL, if the content changes the URL changes through a new content hash. The combination of one-year max-age and immutable ensures browsers cache aggressively and never waste bandwidth revalidating assets that cannot change.

stale-while-revalidate=seconds: an extension directive that allows caches to serve stale responses while asynchronously fetching a fresh version from the origin. Cache-Control: max-age=3600, stale-while-revalidate=86400 tells caches that after the 3600-second freshness window expires they may serve the stale response for up to 86400 additional seconds, while simultaneously fetching a fresh response in the background. The user receives an immediate response from cache, no waiting for origin revalidation, and the cache updates for subsequent requests.

Stale-while-revalidate improves perceived performance for content that changes infrequently, users always receive fast cached responses while the cache stays reasonably current through background updates.

stale-if-error=seconds: allows caches to serve stale responses when the origin returns an error or is unreachable. Cache-Control: max-age=3600, stale-if-error=86400 tells caches that if the origin is unavailable stale responses up to 86400 seconds old may be served. Improves availability during origin outages, users receive slightly outdated content rather than error pages.

Request Cache-Control directives

Clients can also include Cache-Control directives in HTTP requests, instructing caches about their preferences for that specific request. Request directives are less commonly used but important to understand.

no-cache in requests: a browser’s no-cache request directive forces all intermediate caches to revalidate their stored responses with the origin before serving. A user who performs a hard refresh, Ctrl+F5 in most browsers, sends Cache-Control: no-cache in the request, instructing all caches along the path to bypass their stored responses and fetch fresh content. This is how hard refresh bypasses CDN caching as well as browser caching.

no-store in requests: instructs caches not to store the response to this specific request. Used by browsers in some privacy-sensitive contexts to prevent caching of responses to particular requests.

max-age=0 in requests: a browser wanting to force cache revalidation without the full no-cache semantics may send Cache-Control: max-age=0: effectively declaring that any cached response older than zero seconds is unacceptable and must be revalidated.

max-stale and min-fresh: request directives specifying tolerance for stale responses or requiring minimum remaining freshness. Less commonly used in browser contexts, more relevant to HTTP clients in API and programmatic contexts.

Cache-Control and redirects

Cache-Control headers on redirect responses have specific implications for how redirects are cached and how quickly redirect destination changes propagate.

301 permanent redirect caching behaviour: browsers treat 301 responses as permanently cacheable by default, a 301 without explicit Cache-Control headers is effectively cached indefinitely by browsers. This default permanent caching is appropriate for truly permanent redirects, a brand domain that will always redirect to the primary domain, but problematic for redirects where the destination might change.

Adding explicit Cache-Control headers to 301 responses overrides the default permanent caching. Cache-Control: max-age=3600 on a 301 response causes browsers to cache the redirect for one hour, after which the browser revalidates the redirect by requesting the source URL again. If the redirect destination has changed the browser receives the updated redirect response.

302 temporary redirect caching behaviour: browsers do not cache 302 responses by default. Every request for a 302-redirected URL goes through the redirect server, enabling accurate analytics tracking and ensuring redirect destination changes propagate immediately. A 302 can be made cacheable by adding explicit Cache-Control headers, Cache-Control: max-age=3600 on a 302 response, but this is rarely appropriate since the purpose of a 302 is typically to indicate temporary state.

CDN caching of redirect responses: CDN edge servers cache redirect responses according to Cache-Control headers, just as they cache other responses. A 301 redirect with Cache-Control: max-age=86400 served through a CDN is cached at edge nodes for 24 hours. Changing the redirect destination requires CDN cache invalidation, explicitly purging the cached redirect from edge nodes, to propagate immediately. Without cache invalidation the old redirect destination is served from edge caches until the max-age expires.

Redirect management platforms that use CDN infrastructure typically provide cache invalidation tools, purging cached redirect responses when rules are updated to ensure immediate global propagation.

Recommended Cache-Control for different redirect scenarios:

Permanent redirects with no expected destination changes, Cache-Control: max-age=31536000: one year browser caching. The redirect is genuinely permanent, brand domain to primary domain, HTTP to HTTPS.

Permanent redirects where destination might eventually change, Cache-Control: max-age=86400: 24 hour caching. Long enough for performance benefits but short enough for changes to propagate within a day.

Temporary campaign or seasonal redirects, Cache-Control: max-age=3600: one hour caching. Campaign destinations change frequently, short cache duration enables rapid updates.

Analytics-tracked redirects, Cache-Control: no-store: no caching. Every click must hit the redirect server for tracking. Performance cost is acceptable for the analytics benefit.

Cache-Control for different response types

Different categories of web content warrant different Cache-Control strategies, matching cache duration and behaviour to the content’s freshness requirements.

Versioned static assets: Cache-Control: max-age=31536000, immutable. CSS, JavaScript, fonts, and images with content-hash filenames. The URL changes when content changes, the cached version at any URL is always correct for that URL. One-year aggressive caching with immutable prevents any revalidation overhead.

Unversioned static assets: Cache-Control: max-age=86400. Images and resources without content-hash filenames that change occasionally. One day caching balances freshness with reduced origin requests.

HTML pages: Cache-Control: no-cache or Cache-Control: max-age=300. Page content changes with every content update. No-cache ensures freshness through conditional requests. Short max-age, 5 minutes, provides brief browser caching while ensuring reasonably fresh content.

API responses, public data: Cache-Control: public, max-age=300, s-maxage=3600. Public API data that many users request. Browser caches for 5 minutes. CDN caches for 1 hour, reducing origin load significantly.

API responses, user-specific: Cache-Control: private, max-age=300. User-specific data should be browser-cached for performance but not shared through CDN caches.

Authenticated responses: Cache-Control: private, no-cache. Authenticated pages should not be shared through CDN caches. No-cache ensures conditional validation on every load, serving from browser cache when content is unchanged.

Common Cache-Control mistakes

Using no-cache when no-store is intended: confusing no-cache, which permits caching with mandatory revalidation, with no-store, which prevents caching entirely. No-cache is appropriate for content that changes frequently but benefits from conditional request efficiency. No-store is appropriate only for genuinely sensitive data.

Missing Cache-Control on redirect responses: not setting Cache-Control on 301 responses, allowing browsers to cache them indefinitely. If the redirect destination ever needs to change all cached 301s become stale, users are sent to the old destination until browser caches are cleared. Always set explicit Cache-Control on redirect responses.

s-maxage without public: setting s-maxage on responses to authenticated requests without adding public. Shared caches ignore s-maxage on authenticated responses unless public explicitly overrides the default no-shared-cache behaviour.

Overly aggressive no-store: applying no-store to all dynamic content as a precautionary measure. No-store has a real performance cost, every request fetches the full response from the origin. Most dynamic content can use no-cache, which provides freshness guarantees through efficient conditional requests rather than full response retransmission.

Cache-Control inconsistency across related resources: setting different Cache-Control values on HTML pages and their dependencies, CSS, JavaScript, resulting in situations where the HTML is fresh but its resources are stale or vice versa. Coordinated cache control across related resources ensures consistent behaviour.

Related terms

Related terms

Ready to keep every link alive?

Ready to keep every link alive?

Ready to keep every link alive?