Demystifying Edge TTL in Cloudflare
Hello World! (pun very much intended)
Long-time lurker, first time poster. I work as a Managed Services Engineer here at Episerver and a common thing we tackle is CDN optimizations in DXP projects and I thought I'd share some general information and basic things to look for when optimizing cache utilization in Cloudflare.
Out of the box, Cloudflare caches the file formats they mention in their docs here, where it acts as a cache to bring assets geographically closer to the end-user to both offload the origin web server as well as speed things up for the user. Cloudflare has around 194 Points of Presences (PoPs) around the globe.
The worst performance offenders are often images as they tend to be the largest assets so getting them as close to the user as possible makes sense, and many sites use image resizing to display one original image on multiple places in different dimensions, and that resize operation is quite resource intensive and consumes a substantial amount of CPU time. The name of the game becomes,
1. Make sure assets are properly cached in the CDN
2. Make sure assets are cached for as long as possible to keep down revalidations (where the CDN has to re-fetch the asset from the origin web server)
Figuring out what is and what is not cached is fairly straight forward because exposes the response header CF-Cache-Status to us, which should return HIT if an asset is served by the CDN cache. See their docs here for more possible responses. Note that you will get a few MISSes before a HIT as it takes some requests for cache to warm up. If you notice constant MISSes and never get a HIT in the CF-Cache-Status header, or if it returns DYNAMIC, your asset might not get cached as expected. "Dr. Flare" is a neat browser plugin to figure out what is and what is not cached and served by Cloudflare.
So how do we tell Cloudflare what to cache and set Time-To-Live (TTL) values to dictate how long an asset is cached? First off the file has to be in the supported format as noted previously and the web server has to return "public" in the Cache-Control header in order to get cached. The TTL is then dictated by max-age is the cache-control header, or it's derived from the Expires header and then displayed in the cache-control header as Max-Age=Expires-Date (seconds).
In the below example, no modifications have been done to the Episerver Alloy template and it defaults to using the Expires header with a 12-hour (43200 seconds) expiration time.
Response from Cloudflare (max-age derived from Date and Expires):
(Invoke-WebRequest "https://www.domainthatgoestocloudflare.com/contentassets/e6c47a7021e64c288fd79956fb477a50/alloymeetbanner.png").headers
Key Value
--- -----
Transfer-Encoding: chunked
Connection: keep-alive
CF-Cache-Status: HIT
Age: 3
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
CF-RAY: 58b1277f8e18caf8-ARN
cf-request-id: 02629703b50000caf851ac8200000001
Cache-Control: public, max-age=43200
Content-Type: image/png
Date: Tue, 28 Apr 2020 13:33:18 GMT
Expires: Wed, 29 Apr 2020 01:33:15 GMT
ETag: "1D5FD02B5411180"
Last-Modified: Wed, 18 Mar 2020 08:53:35 GMT
Set-Cookie: __cfduid=d846441b2dd1d7fc2d424f6dc8dfaa81b1588080798; expires=Thu, 28-May-20 13:33:18 GMT; path=/; domain=....
Server: cloudflare
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Response from origin web server:
(Invoke-WebRequest "https://www.domainthatbypassescloudflare.com/contentassets/e6c47a7021e64c288fd79956fb477a50/alloymeetbanner.png").headers
Key Value
--- -----
Transfer-Encoding: chunked
Accept-Ranges: bytes
Cache-Control: public
Content-Type: image/png
Date: Tue, 28 Apr 2020 13:34:15 GMT
Expires: Wed, 29 Apr 2020 01:34:14 GMT
ETag: "1D5FD02B5411180"
Last-Modified: Wed, 18 Mar 2020 08:53:35 GMT
Set-Cookie: ARRAffinity=e72ac5b17c6574f3ced950f54941e20cb3d62e26a50b6ece889ededb334459e9;Path=/;HttpOnly;Domain=...
Server: Microsoft-IIS/10.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
12 hours could be sufficient, but if we have hundreds of gigabytes of data that should be revalidated every 12 hours by ~200 PoPs, it gets inefficient very quickly. Mid-tier caching allows PoPs to share data between one another to offload the origin, but 12 hours is a bit low so let's bump that up.
There's two ways of accomplishing this - either increase the time of the Expires header or remove it altogether and set a static max-age in the cache-control header instead. There is an easy way of accomplishing this with staticFile as it allows us to increase the value of Expires with just a few lines in web.config https://world.episerver.com/documentation/developer-guides/CMS/configuration/Configuring-staticFile/.
<configSections>
<!-- breaking change in CMS 11 https://world.episerver.com/documentation/upgrading/Episerver-CMS/cms-11/breaking-changes-cms-11/ -->
<section name="staticFile" type="EPiServer.Framework.Configuration.StaticFileSection, EPiServer.Framework.AspNet" allowLocation="true" />
</configSections>
...
<!-- Set Expires header for assets in path /contentAssets -->
<location path="contentAssets">
<staticFile expirationTime="30.0:0:0"/>
</location>
After applying the above configuration to web.config (and purging the cache), we can see that the Expires header is now 30 days instead of 12 hours and max-age updates accordingly from Cloudflare.
(Invoke-WebRequest "https://www.domainthatgoestocloudflare.com/contentassets/e6c47a7021e64c288fd79956fb477a50/alloymeetbanner.png").headers
Key Value
--- -----
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept
CF-Cache-Status: HIT
Age: 3
Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
CF-RAY: 58b1451d1d0b75f8-ARN
cf-request-id: 0262a9862e000075f8c383a200000001
Cache-Control: public, max-age=2073597
Content-Type: image/png
Date: Tue, 28 Apr 2020 13:53:31 GMT
Expires: Fri, 22 May 2020 13:53:28 GMT
ETag: "1D5FD02B5411180"
Last-Modified: Wed, 18 Mar 2020 08:53:35 GMT
Set-Cookie: __cfduid=d0fadf111b66d8b5685ad342c62c9393b1588082011; expires=Thu, 28-May-20 13:53:31 GMT; path=/; domain=.....
Server: cloudflare
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
But of course, with great caching power comes great caching responsibility and your mileage may vary with some things.
1. As with any other caching system, we have to consider cache evictions when assets gets updated. Out of the box, IIS includes the ETags header, but there are other ways to also evict cache such as versioned URLs. See our docs on the subject here. I'd encourage anyone to play around with different solutions and see what works best for you and your editors.
2. If you use any third-party plugin, such as ImageResizer or have a custom HTTP module, you might find yourself having to modify headers through that instead. For instance, ImageResizer has its own configuration as described here.
We really need a Cloudflare API so we can purge individual items.