How to leverage the browser cache with a CDN

An introduction to multi-level caching.

maze

Since a content delivery network (CDN) is essentially a cache, you might be tempted not to make use of the cache in the browser, to avoid complexity. However, each cache has its own advantages that the other does not provide. In this post I will explain what the advantages of each are, and how to combine the two for the most optimal performance of your website.

Why use both?

While CDNs do a good job of delivering assets very quickly, they can’t do much about users who are out in the boonies and barely have a single bar of reception on their phone. As a matter of fact, in the US, the 95th percentile for the round trip time (RTT) to all CDNs is well in excess of 200 milliseconds, according to Cedexis reports. That means at least 5% of your users, if not more, are likely to have a slow experience with your website or application. For reference, the 50th percentile, or median, RTT is around 45 milliseconds.

So why bother using a CDN at all? Why not just rely on the browser cache?

  1. Control. With most CDNs, you have the option to purge your assets from their cache, something that’s very useful when you make changes to your assets. You do not have this option with the browser cache.
  2. First impressions matter. The browser cache doesn’t help in any way the first time a user visits your site, since it’s cold (empty of useful objects). CDNs make the user experience with a cold browser cache as fast as possible, and then a warm browser cache will make consecutive pages even faster.
  3. CDNs still have a geographical advantage. Even if a user falls in the 95th percentile, a 250 millisecond RTT is still better than a 350 millisecond RTT. Especially when you consider that every asset will take at least one round trip on an open connection, but you need an additional round trip to open said connection. TLS (the successor to SSL) adds at least one more round trip per connection, sometimes two. All these extra round trips start stacking up quite fast.
  4. CDNs are much more efficient, since their cache is shared between all of your users. This means less load on your origin servers, without the need to have a caching layer of your own in your platform. Though, if you’re using multiple CDNs, such a caching layer in the form of a simple Varnish box is not a terrible idea. Check out this Varnish tutorial if you’re interested in learning more.

How to use both

Now that we’ve determined a CDN is still important and that the browser cache is also quite valuable, here are the two approaches I would recommend to combine the two:

  1. A short time-to-live (TTL) for the browser cache, combined with long TTLs and purging on the CDN side.
  2. A long TTL for both the CDN and the browser cache, but with version-based cache busters.

Below, I’ll go through each approach in detail, and then discuss how you could even use a combination of the two.

Short & long TTLs

Ideally you would use a TTL for the browser cache that covers a whole visit, but not much more. That way, your users have speedy pageloads all throughout their visit, but don’t end up with outdated assets when they return later. Your analytics tools should be able to tell you what the visit times look like for your site, but generally 5 or 10 minutes is a good ballpark.

If your CDN supports purging, something like a month or a year works well as the TTL. Keep in mind that some CDNs purge very quickly, so you shouldn’t send the purge command until the new versions of the assets you’re updating are guaranteed to be served by the origin. I’ve witnessed cases where someone sent a purge before the file was synced to the web servers, which led to a lot of confusion.

To set the browser cache TTL, use a Cache-Control header in your origin response. Then use either a header like Surrogate-Control or Edge-Control, or an override in your CDN configuration, to set the TTL there.

For example:

Cache-Control: max-age=600
Surrogate-Control: max-age=31536000

Check your CDN’s documentation to find out whether they support a TTL override in a header, and how to use it.

Version-based cache busters

Despite the name, cache busters can actually improve caching when used wisely. A cache buster is simply a new query string parameter that is added to the URL. Web servers and most application servers simply ignore query string parameters that they’re not interested in. But by default, caches have to assume that any difference in the query string will have influence on the result, and have to treat https://www.example.com/css/main.css,
https://www.example.com/css/main.css?cb=foo and https://www.example.com/css/main.css?foo=bar as three unique objects. Even if the origin returns the exact same response for all of them. Common practices are to use either a (short) hash of the file content, a build number, or commit hash.

My personal preference is to use a hash of the file content. That way, the cache buster only changes when the content changes. With build numbers or commit hashes, the cache buster changes whenever anything changes. However, since all the URLs of assets on your site have to have the cache buster, it can be easier to use the build number or commit hash.

The downside to this approach is having to update all of the asset URLs for each change. The upside is that you can have your assets cached in the browser with nice long TTLs, and still have the browser fetch fresh assets when its cache is out of date.

Mix and match

Depending on how fast your CDN can purge content, you should consider putting all of your HTML pages on your CDN as well. However, even if you can reengineer your site to use cache busters for assets like images, stylesheets, and JavaScript, you should never use cache busters on URLs for the actual pages themselves — search engines like Google will penalize you for having query strings in page URLs.

If you’re putting your pages on your CDN, consider using the short and long TTL technique for your pages, and cache busters for the assets used by said pages.

Advanced bits: revalidation

A very nice side effect of using the browser cache is that browsers will keep objects around even if they’re expired, and send additional revalidation headers with their requests. If the object being requested has not changed, your CDN can simply respond with a 304 Not Modified status, which has no body, telling the browser it can use the expired object and optionally provide a new TTL.

While each request that gets a 304 response still takes a round trip to complete, the lack of the response body means quite a bit of bandwidth savings. Not only is that a benefit to users on slow connections, it might also reduce your monthly CDN payments.

To make revalidation work, all you have to do is make sure your origin includes a Last-Modified or ETag header in its responses. The good news is that most web servers already include Last-Modified and ETag headers for any static files they serve from disk. The value of the Last-Modified header is based on the file’s modification time. The value of the ETag header is based (in Apache) on modification time, inode number and size.

When a browser notices one of these two headers, or both, on expired objects in its cache, it will add an If-Modified-Since header with the value of Last-Modified and add an If-None-Match header with the value of ETag. An object is considered unchanged if the values of If-None-Match and ETag are the same, and if the value of If-Modified-Since either matches or is after the value of Last-Modified.

If you have a single web server for your static assets, you probably already have revalidation working perfectly, due to the common defaults.

However, if you use multiple web servers for redundancy, and those servers each have local storage instead of shared storage, you could be causing revalidation to fail randomly. Because you can’t guarantee a specific file is assigned the same inode number on each web server, the ETag header generated for it will be different from server to server. And most deploy scripts do not preserve modification time when copying files, which means the Last-Modified header will differ as well.

This is bad for two reasons. First, if your CDN uses revalidation when talking to your origin, there could be a lot of unneeded bandwidth being wasted on full responses instead of 304 responses. Second, your CDN could have different values of ETag and Last-Modified on different servers. Since browsers aren’t guaranteed to talk to the same server for every request, they could also get unneeded full responses because of a mismatch in values.

To make optimal use of revalidation if you have multiple web servers with local storage, I would recommend to turn off ETag in favor of using Last-Modified exclusively and make sure that your deploy script preserves modification time when copying files.

Your mileage may vary

Not all CDNs are created equal, so while all the things I discussed in this post are standard, or at least very common, you should still make sure your CDN supports them. Or if you don’t have a CDN yet and are shopping around for one, you now know some features to look for.


Editor’s note: If you are new to CDNs, everything in this post and more will be covered in-depth by Rogier Mulhuijzen and Austin Spires during their training, Integrating CDNs With Your Stack for Improved Performance and Scalability, on May 27th at Velocity Santa Clara.

Public domain maze image via pixabay.

tags: , , , , , ,