Getting your HTTP Cache-Control headers right
Published: Sat Nov 27 2021Caching plays a fundamental role in performance. What’s quicker than a fast request? No request. Getting your caching strategy right can make a huge difference to your users - both repeat and first time.
“There are only two hard things in Computer Science: cache invalidation and naming things.” (Phil Karlton)
25 years later, this well-known saying still holds true and while I cannot help you with the naming, I want to share what I know about caching to make it easier for you to decide on a caching strategy. Disclaimer: The scope of this article isn’t to serve as a comprehensive guide to all possible caching strategies and how they work - there’s already plenty of content like that around. My goal is to provide you with a reference which you could use to answer the question, “How should I cache this resource?”
What is caching?
A cache is a store where you could put a resource. This could be done to avoid having to recalculate an expensive computation or to place the content closer to the user. In websites, this is dictated by the Cache-Control
header; which is defined in the HTTP/1.1 specification.
Note: The Expires
header is no longer needed as its behavior is covered by the Cache-Control
header.
Private and shared caches
In addition to caching in your browser, if your content is behind a CDN then your cache-control
headers influence how the CDN caches your content on the edge. Browser cache is referred to as a private cache, while a CDN cache is referred to as a shared cache. The cacheability of a resource is set using the directives below:
public
- Any cache may store the response, including a CDN.private
- The response is intended for a single user and should only be stored by the browser cache.no-store
- Should not be stored on any cache.
Demo
To test the different cache-control
headers, I have created a repository containing a simple NodeJS server. Please feel free to explore the source code on GitHub and run the application locally.
Static assets
Static assets refer to files whose contents do not change and are not generated on request. This includes scripts, stylesheets, and images. As these files are generated at build time, most modern build tools can assign a unique hash to each file. This hash serves as a unique fingerprint for each version of the file and is included in the resource’s URL. This could be in the form of a query string parameter (e.g. app.js?v=c4d27698
), or filename (e.g. index-0e6a410d.js
). Having a unique URL means that you can cache these assets for a very long time, even up to one year.
cache-control: max-age=31536000
When the website is redeployed and the contents of one of the source files have changed, then it will be assigned a new URL and create a new cache entry. If the files are served through a CDN, then the CDN will cache this file. Most CDNs will purge the old files on each redeployment.
- Freshness - Guaranteed.
- Performance - First user to request the file will receive it from the origin server. Every other user will receive a cached copy.
Caching the HTML page
One of the major headaches with caching is your HTML page. Generally speaking, your HTML page will make requests for other resources which are needed to render the page, such as CSS, images and JavaScript files. Whenever your website is redeployed, it will update the references to the static assets within the HTML document.
<link rel="stylesheet" href="/style.v1.css" />
becomes
<link rel="stylesheet" href="/style.v2.css" />
So if the HTML document is cached, users with a cached copy will request the old stylesheet, style.v1.css
; which no longer exists on the origin server. So while style.v1.css
is likely to be cached on the user’s browser because of its long expiry, it is possible that some users encounter a broken website too. If the cached resource style.v1.css
is removed from the cache before the cached HTML resource, the user will see a broken layout.
To complicate matters, your HTML may also contain server-side rendered content retrieved from a CMS. If the content is updated on the CMS, the cached HTML is now stale.
Scenarios
When deciding your caching strategy, it is usually a trade-off between performance and freshness. Instead of explaining the different cache-control
directives and their applications, I will be describing some real-life scenarios I worked with and how I would set my cache-control
headers for that type of website.
Blog
If you are looking to cache your blog, the chances are that the content is not time-sensitive and you would prioritize performance over freshness. Users are more likely to go directly to the article page instead of navigating through the website. If they revisit your blog, they are likely to return when a new article is published - that is after the cache has expired. The CSS and JavaScript files probably do not change that often either.
cache-control: public, max-age=600, stale-while-revalidate=60
The blog you’re reading uses a public
, max-age
, and stale-while-revalidate
combination. By applying a max-age
of 600 (10 minutes), you can cache the page for the duration of the user’s session. If they leave the page and return within 10 minutes, it would load instantly.
The public
directive states that the resource may be cached on both private and shared caches. Therefore if the resource has expired in the user’s browser cache, it would request it from the CDN. The CDN may have the resource cached and serve it instantly. If it has expired on the CDN, then it would fetch it from the origin and update the caches.
Using stale-while-revalidate
means that the resource can be served stale for an additional 60 seconds after it has expired while it is being revalidated.
In my specific case I am able to cache the HTML file rather aggressively as I ship very little JavaScript - none of which is critical to the user experience - and the critical CSS is inlined in the HTML file. Depending on how important your subresources are to the user experience, you may want to cache for a shorter period. If your blog does not work at all without JavaScript, then you might want to reduce the max-age
to avoid the race condition described earlier.
- Freshness - Users may receive a worst-case 11-minute old response.
- Performance - The first user to request the resource will wait for it to be fetched from the origin. If the user returns within 11 minutes, they will receive a cached response. If they return after 11 minutes, they will be served a cached copy from the CDN. If no users visit the website for 11 minutes, the first user to visit the website will wait for the resource to be fetched from the origin.
News website
A news website is updated frequently, with new articles being added to the home page and the content of some articles updated several times per day.
cache-control: public, max-age=30, stale-while-revalidate=30, stale-if-error=600
By applying a combination of max-age
, stale-while-revalidate
, and stale-if-error
you can always serve content quickly while minimizing the risk of stale data. Resources will be cached on shared or private caches for 30 seconds. When the resource expires, the cache will serve the stale data for another 30 seconds. The stale-if-error
directive indicates that if the origin server does not respond when revalidating the resource, the cache may serve the stale data for another 10 minutes.
Note: The stale-if-error
directive isn’t widely supported on the browser but is supported on most CDNs.
- Freshness - In the worst case, the user may receive a 1-minute old response. The
max-age
andstale-while-revalidate
values may be adjusted to calibrate this worst-case scenario. - Performance - The first user to request the resource will wait for it to be fetched from the origin. If the same user or any other user revisits the page within 30 seconds, they would get a cached response. If they visit the page beyond 30 seconds but within 60 seconds, then they would get an instant (stale) response while the cache is revalidated. If they revisit the page beyond 60 seconds, then the resource is fetched from the CDN. If no user visits the page for 1 minute, the next user will fetch the resource from the origin.
Live event
If your website is showing a live event and content is updated frequently, you want the data to be as fresh as possible. An example of a live event may be a sports match showing real-time scores or an election results page.
cache-control: public, no-cache
Setting no-cache
indicates that a cache must not be used for a subsequent request without revalidating it with the origin server. This is the equivalent of must-revalidate, max-age=0
. Once again, setting public
will enable your CDN to cache the resource and avoid hitting your origin server for each user request.
- Freshness - Users will always receive a fresh resource.
- Performance - Each request will incur a network call. If the resource has not changed, then the payload would only include the response headers.
Logged-in pages
Any page or resource which is only available for a logged-in user or contains data specific to a logged-in session should not be cached.
cache-control: no-store
- Freshness - Users will always receive a fresh resource.
- Performance - Each request will be fetched from the origin.
Conclusion
HTTP Cache-Control headers remain a cornerstone for good performance. When choosing a caching strategy, take your time to consider the following:
- Not all resources are equal.
- Measure the performance gains a user would benefit from if you cache a resource.
- Can you get away with serving stale content?
- Identify how likely your cached resources may become out of sync and what your users would experience in that eventuality.
Recommended reading
There is a lot of content explaining how the different cache-control
directives work. Below are some of my favorite resources on the topic.
Latest Updates
- Improving Largest Contentful Paint on slower devicesSat Mar 09 2024
- Learn Performance course on web.devWed Nov 01 2023
- Setting up a Private WebPageTest instanceMon Jun 26 2023
- First Important Paint - Developing a custom metricSat Jun 10 2023
- Web Performance AuditWed Jun 07 2023