Getting your HTTP Cache-Control headers rightPublished: Sat Nov 27 2021
Caching 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.
Expires header is no longer needed as its behavior is covered by the
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.
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 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.
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
<link rel="stylesheet" href="/style.v1.css" />
<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.
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.
cache-control: public, max-age=600, stale-while-revalidate=60
The blog you’re reading uses a
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.
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.
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.
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.
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
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.
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
stale-while-revalidatevalues 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.
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
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.
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.
- Freshness - Users will always receive a fresh resource.
- Performance - Each request will be fetched from the origin.
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.
There is a lot of content explaining how the different
cache-control directives work. Below are some of my favorite resources on the topic.
- Priority Hints and optimizing LCPMon Jan 02 2023
- Google IO Extended - MaltaWed Sep 07 2022
- Interaction to Next PaintThu Jun 16 2022
- Component composition in ReactWed Feb 23 2022