Cache-Control Recommendations
Cache-Control
is one of the most frequently misunderstood HTTP headers, due to its overlapping and perplexingly-named directives. Confusion around it has led to numerous security incidents, and many configurations across the web contain unsafe or impossible combinations of directives. Furthermore, the interactions between various directives can have surprisingly different behavior depending on your browser.
The objective of this document is to provide a small set of recommendations for developers and system administrators that serve documents over HTTP to follow. Although these recommendations are not necessarily optimal in all cases, they are designed to minimize the risk of setting invalid or dangerous Cache-Control
directives.
Recommendations
Recommendation | Safe for PII | Use Cases | Header Value |
---|---|---|---|
Don't cache (default) | Yes | API calls, direct messages, pages with personal data, anything you're unsure about | max-age=0, must-revalidate, no-cache, no-store, private |
Static, versioned resources | No | Versioned files (such as JavaScript bundles, CSS bundles, and images), commonly with names such as loader.0a168275.js |
max-age=n, immutable |
Infrequently changing public resources, or low-risk authenticated resources | No | Images, avatars, background images, and fonts | max-age=n |
Don't cache (default): max-age=0, must-revalidate, no-cache, no-store, private
When you're unsure, the above is the safest possible directive for Cache-Control. It instructs browsers, proxies, and other caching systems to not cache the contents of the request. Although it can have significant performance impacts if used on frequently-accessed public resources, it is a safe state that prevents the caching of any information.
It may seem that using no-store
alone should stop all caching, but it only prevents the caching of data to permanent storage. Many browsers will still allow the caching of these resources to memory, even if it doesn't write them to disk. This can cause issues where shared systems may contain sensitive information, such as browsers maintaining cached documents for logged out users.
Although no-store
may seem sufficient to instruct content delivery networks (CDNs) to not cache private data, many CDNs ignore these directives to varying degrees. Adding private
in combination with the above directives is sufficient to disable caching both for CDNs and other middleboxes.
Static, versioned resources: max-age=n, immutable
If you have versioned resources such as JavaScript and CSS bundles, this instructs browsers (and CDNs) to cache the resources for n
seconds, while not purging their caches even when intentionally refreshing. This maximizes performance, while minimizing the amount of complexity that needs to get pushed further downstream (e.g. service workers). Care should be taken such that this combination of directives isn't used on private or mutable resources, as the only way to "bust" the cache is to use an updated source document that refers to new URLs.
The value to use for n
depends upon the application, and is ideally set to a bit longer than the expected document lifetime. One year (31536000
) is a reasonable value if you're unsure, but you might want to use as low as a week (604800
) for resources that you want the browser to purge faster.
Infrequently changing public resources or low-risk authenticated resources: max-age=n
If you have public resources that are likely to change, simply set a max-age
equal to a number (n
) seconds that makes sense for your application. Simply using max-age
will allow user agents to still use stale resources in some circumstances, such as when there is poor connectivity.
There is no need to add must-revalidate
outside of the unlikely circumstance where the resource contains information that must be reloaded if the resource is stale.
Directives
For brevity, this only covers the most common directives used inside Cache-Control
. If you are looking for additional information, the MDN article on Cache-Control is pretty exhaustive. Note that its recommendations differ from the recommendations in this document.
max-age=n
(and s-maxage=n
)
- instructs the user agent to cache a resource for
n
seconds, after which time it is considered "stale" s-maxage
works the same asmax-age
, but only applies to intermediary systems such as CDNs
no-store
- tells user agents and intermediates not to cache anything at all in permanent storage, but note that some browsers will continue to cache in memory
no-cache
- contrary to everything you would think, does not tell browsers not to cache, but instead forces them to check to see if the resource has been updated via
ETag
orLast-Modified
- essentially the same as
max-age=0, must-revalidate
must-revalidate
- forces a validation when cache is stale – this can mean that browsers will fail to use a cached resource if it is stale but the site is down
- generally only useful for things like HTML with time-specific or transactional data inside
- if max-age is set, must-revalidate doesn't do anything until it expires
immutable
- indicates that the body response will never change
- when combined with a large
max-age
, instructs the browser to not check to see if it's still valid, even when user purposefully chooses to refresh their browser
public
- indicates that even normally non-cacheable responses (typically those requiring
Authorization
) can be cached on public systems, such as CDNs and proxies - recommended to not use unless you're certain, as it's probably better to waste bytes than to make the mistake of having a private document get cached on a CDN
private
- indicates that caching can happen only in private browser (or client) caches, not on CDNs
- note that this wording can be deceiving, as “private” documents are frequently cached on CDNs, with high-entropy URLs
- documents behind authentication are an example of a good target for the
private
directive
stale-while-revalidate=n
- instructs browsers to use cached resources which have been stale for less than
n
seconds, while also firing off an asynchronous request to refresh the cache so that the resource is fresh on next use - great for services where some amount of staleness is acceptable (e.g. weather forecasts, profile images, etc.)
- can provide a decent performance boost, as long as you're careful to avoid any issues where you require multiple resources to be fresh in a synchronized manner
- browser support is still limited, so if you decrease
max-age
to compensate, note that it will affect browsers that don't yet supportstale-while-revalidate
Common anti-patterns and pitfalls
Surveys of Cache-Control across the internet have identified numerous anti-patterns in broad usage. This list is not meant to be extensive, but simply to demonstrate how complex and sometimes misleading that the Cache-Control directive can be.
max-age=604800, must-revalidate
While there are times that max-age
and must-revalidate
are useful in combination, for the most part this is saying that you can cache a file but then must immediately distrust it afterwards even if the hosting server is down. Instead use max-age=604800
, which says to cache it for a week while still allowing the use of a stale version if the resource is unavailable.
max-age=604800, must-revalidate, no-cache
no-cache
tells user agents that they must check to see if a resource is unmodified via ETag
and/or Last-Modified
with each request, and so neither max-age=604800
nor must-revalidate
do anything.
pre-check=0, post-check=0
You still see these directives appearing in Cache-Control responses, as part of some long-treasured lore for controlling how Internet Explorer caches. But these directives have never worked, and you're wasting precious bytes
Expires: Fri, 09 April 2021 12:00:00 GMT
While the HTTP Expires
header works the same way as max-age
in theory, the complexity of its date format means that it is extremely easy to make a minor error that looks valid, but where browsers treat it as max-age=0
. As a result, it should be avoided in preference of the far more simple max-age
directive.
Pragma: no-cache
Not only is the behavior of Pragma: no-cache
largely undefined, but HTTP/1.0 client compatibility hasn't been necessary for about 20 years.
Glossary
- fresh — a resource that was last validated less than
max-age
seconds ago - immutable — a resource that never changes, as opposed to mutable
- stale — the opposite of fresh, a resource that was last validated more than
max-age
seconds ago - user agent — a user's browser, mobile client, etc.
- validated — the user agent requested a resource from a server, and the server either provided an up-to-date resource or indicated that it hasn't changed from the last request
Learn More
- Caching Tutorial for Web Authors and Webmasters - Mark Nottingham
- Demystifying HTTP Caching - Bharathvaj Ganesan
- HTTP Caching Tests
- RFC 2616 - Cache-Control and other caching in HTTP
- RFC 5681 - Cache-Control Extensions for Stale Content
- RFC 7234 (HTTP Caching)
Footnotes
- Technically not true thanks to HTTP/2 header compression, but don't send them regardless.
[Category: Security] [Tags: Cache-Control]