Configuring Cloudflare's Cache, and Cache-Control Headers
Table of Contents
I’ve talked enough about Cloudflare caching that I’m not going to do introduce it again. This time though, we’re going in depth just a bit more, going over what’s cached, default cache times, and… more headers.
Cloudflare’s Default Cache Policy
First: Cloudflare only caches resources from proxied sites, meaning the ones with an orange cloud in your DNS panel. Resources from third-parties or grey-cloud sites don’t get anything.
Second: Only static content is cached. “Static content” is determined by file extension and not MIME type. Here’s the list of default cached extensions1:
- bmp (Images)
- class (Compiled Java)
- css (Webpage styling and layout rules)
- csv (Tabular structured data)
- doc (Microsoft Word document)
- docx (Microsoft Word document)
- eot (Fonts)
- eps (Graphics / Documents)
- gif (Images)
- ico (Images)
- jar (Compiled Java, entire app)
- jpeg (Images)
- jpg (Images)
- mid (MIDI audio)
- midi (MIDI audio)
- otf (Fonts)
- ps (Graphics / Documents)
- svg (Vector graphics)
- svgz (Vector graphics, compressed)
- swf (Flash app)
- tif (Images)
- tiff (Images)
- ttf (Fonts)
- webp (Images)
- woff (Fonts)
- woff2 (Fonts)
- xls (Microsoft Excel spreadsheet)
- xlsx (Microsoft Excel spreadsheet)
robots.txt will be cached.
On top of this, there’s a few other rules:
Default Cache Times
By default, positive response codes like
204 No Content, and
206 Partial Content, are cached for 120 minutes (two hours).
301 Moved Permanently,
302 Found, etc.) are cached for 20 minutes.
404 Not Found and
410 Gone, and similar errors are cached for only 3 minutes.
405 Method Not Allowed, and
500 (server) series errors only last for just 1 minute.
Note that these can be overridden with a control header, but we’ll get to that later.
Max Cache Size
512 MB per resource, or a flat 5 GB for enterprise customers. But if you’re serving files larger than that, I think there’s bigger things to worry about than CDN cache.
With every request that goes through Cloudflare, a
CF-Cache-Status header is added on the response, that details what the cache behavior was for that resource.
Here’s what the values mean:
HIT: Resource was cached and used.
MISS: Resource is cache-eligible but was not cached, and had to be fetched from the origin server. Subsequent requests should be cached.
EXPIRED: Resource was cached past it’s TTL (Time To Live), and was served from the origin server, then updated in the cache.
STALE: Resource cached past TTL, but could not be fetched from the origin server. Instead, the potentially out-of-date cached version was used.
BYPASS: The origin
Cache-Controlheader explicitly instructed Cloudflare to not cache the resource.
REVALIDATED: Resource served from cache while stale, and was verified to still be valid by a conditional
UPDATING: Resource served from cache while expired, because the cache was busy retrieving an updated copy from the origin server.
DYNAMIC: Cloudflare is not going to cache this resource with the current configuration.
Cache-Control header (or
A server can send a
Cache-Control header with many different directives that specify who can cache that response, how long it should be cached for, and what to do when that time runs out.2
I use a relatively simple one that looks like this (on everything):
Cache-Control: public, max-age=172800, stale-if-error=604800
172,800 seconds = 2,880 minutes = 48 hours = two days.
604,800 seconds = 10,080 minutes = 168 hours = 7 days = one week.
Any cache may store the response for a maximum of two days without revalidation, and if unable to revalidate, stale responses are permitted for up to one week.
Now technically this is incorrect, as my content can change daily, meaning browsers might cache parts of the page that have indeed changed. However, because of how most browsers behave, this is fine, they have their own rules for cache eligibility.
public bit at the front means that a shared cache is allowed to store the resource requested, meaning that proxy caches are allowed to store it, meaning that Cloudflare is allowed to store it.
private would be the opposite here.
It doesn’t specify anything special either, such as revalidation behavior. I’m leaving that up to the client device.. for now.
Cache-Control header specifies either
no-store3, or similar mechanisms, then this is what causes a
BYPASS cache status.
Pragma was a rather general-purpose HTTP/1.0 header… in theory.
In reality, it only received one directive:
Only used for backwards-compatibility,
Pragma: no-cache has the same effect as
Real-World Performance Test
Let’s do a quick test: How much of this site is eligible for caching? I’ll be doing two tests: homepage, and the post on Switch accessories, because it has a lot of images, and pages have slightly different resources to load.
Of this I’ll measure cache eligibility on two fronts: per-request and total size.
Cache eligible resources are those that do not have a
DYNAMIC cache status header (
BYPASS cannot be sent for now), cache ineligible resources do have
DYNAMIC as status, and external resources are ones with no
CF-Cache-Control at all.
|1.93 MB||20.3 kB||65.9 kB|
|2.64 MB||25.3 kB||263.4 kB|
The only ineligible items across both tests were the main page HTML, and the
.webmanifest file for Android phones.
While Gravatar, Ko-fi, Google’s font data, and Disqus are all (expectedly) external, I did notice that some external sites (e.g.,
c.disquscdn.com) are also sending CF headers, meaning that they go through Cloudflare too.
Since I’m making a point about the total effectiveness of Cloudflare, I’m including these results in the “Cache-eligible” column.
no-cachemeans that responses must check that the copy is valid and up-to-date before using it, effectively just turning cache off.
no-storemeans that it cannot store the response, period, as opposed to not being able to use it without checking first. The former effectively disables caching, and the latter explicitly disallows it. ↩︎