I had been unhappy with my website’s speed for quite some time. You see, vladiliescu.net is a small, static website, generated by Hugo from a Github repo and hosted on Netlify. Oh, and served by Cloudflare.

For the longest time, the site felt juuust a bit sluggish, exactly annoying enough to notice but not annoying enough to investigate further. So that’s what I did for a while, ignoring the speed and focusing on writing my machine learning articles. After all, what’s the point of having a fast website if there’s nothing worth reading there?

Plus, I had been using Cloudflare as my DNS/CDN solution of choice for years. And I had made sure to let it handle all my caching, minify all there was to minify, Brotli compress, Rocket Load, all that stuff that’s so fun to tweak when you’re trying to avoid doing actual work. If Cloudflare with all it’s might can only deliver a “meh” speed, then clearly I had no business interfering and trying to make it better.

Until one day. I had decided to try out Azure Static Web Apps to see how it compared with Netlify. SWA was easy enough to set up, even though it required some outrageous1 permissions. Once set up though, it struck me just how fast everything loaded. Not instant, mind you, but faster than what I had been used to. And with no fancy CDN in-between, just a simple, static website. It was then that I decided to fix this once and for all.

But how will it end?

First thing I did was to measure the response times. Since my beef was with the HTML document (and not the .js/.css files), I only measured those, by either issuing full reloads (Shift+Command+R on everything but Safari), or simple reloads (Command+R).

When accessing the Static Web App instance directly I would get around 50ms of delay. When accessing the Netlify instance, it was 50-60ms. Accessing the Netlify instance via Cloudflare was anywhere between 100ms to 300ms. But mostly around 200ms. No caching. No nothing.

I couldn’t believe my fancy CDN which served compressed and optimized everything could add so much delay just for its trouble. I made a swift decision - my trust betrayed, I would ditch Cloudflare and switch to Azure DNS and CDN. Revenge would be mine, no doubt.

But I couldn’t stop wondering why this happened though. Wasn’t CF supposed to cache everything? We’re talking about a static website here, there’s nothing dynamic about it!

Why Does Cloudflare Slow Down My Website?

Well, actually, it’s complicated. After some googling I found that by default Cloudflare will cache your resources including .js and .css files, as expected. It will not, however, cache HTML, so you’ll be left with waiting for it to download for every single request. A reasonable default, for reasonable people. The unreasonable people however, can tell it directly to cache HTML, which is exactly what I did.

I created a custom page rule, setting the Cache Level to Cache Everything for the entire domain. And, with a newly found appreciation for actually understanding what I’m doing, I proceeded to test the loading speed.

It didn’t go well. The speed was more or less the same as before.

Not cool

This time however, I really really really wanted to know why.

Some further googling revealed several people complaining about the same thing, with helpful strangers suggesting they should check a particular response header, cf-cache-status. That’s the header Cloudflare uses to show whether a resource is cached, and you generally want it to be HIT. Not MISS, not EXPIRED, not STALE, but HIT. In my case, it was REVALIDATED. 🤦‍♂️

Apparently, Cloudflare will try to cache everything if you tell it so, but with some exceptions, as documented here:

By default, Cloudflare respects the origin web server’s cache headers in the following manner unless overridden via an Edge Cache TTL Page Rule:

  • If the Cache-Control header is set to private, no-store, no-cache, or max-age=0, or if there is a cookie in the response, then Cloudflare does not cache the resource.
  • Otherwise, if Cache-Control is set to public and the max-age is greater than 0, or if the Expires header is a date in the future, Cloudflare caches the resource.
  • If both max-age and an Expires header are set, max-age is used.

Interesting thing, that max-age=0. I say interesting, because when I looked at Netlify’s response headers, lo and behold, cache-control: public, max-age=0, must-revalidate. So Netlify is actively telling Cloudflare that all content should be cached, but also revalidated. You know, just in case.

How to Fix Caching with Cloudflare Page Rules

The solution is right there, in the text. “unless overridden via an Edge Cache TTL Page Rule”. There are some caveats that come with activating an Edge Cache TTL rule, most important being that it removes all cookies from the origin web server. Like authentication cookies. Or other cookies. Any cookies you send, really. Little static websites don’t use cookies though, so I went on and activated it.

Edge cache..so hot right now

Aaaaaand it worked! I couldn’t believe it at first but it worked, everything cached, 0ms, cf-cache-status=HIT and all that.

Alternative: How to Fix Caching with Netlify Headers

But what happens if we want to set different caching policies for each type of resource (.js, .css, etc.). Or for different paths in our website? The free version of Cloudflare only offers up to 3 page rules, so we won’t be able to do much with that.

Enter Netlify custom headers. They can be used to set all sorts of headers, including caching. All you need to do to use them is create a very simple _headers file in your static folder, and create as many rules as you like.

/* 
    cache-control: public
    cache-control: max-age=86400

/styles/*
    cache-control: public
    cache-control: max-age=604800

Just remember to keep that Cache Everything Cloudflare rule, otherwise you won’t get any HTML cached, just like when we started this whole story.

For a more in-depth trip to Netlify custom headers, read this excellent guide.

In Conclusion

Well, this was a fun adventure.

Yes it was

I started off wondering why my site was somewhat sluggish, and ended up configuring Netlify custom headers and an all-encompassing CloudFlare caching policy. In all honesty I should have done this ages ago, but I’m glad I did it now. vladiliescu.net loads faster than ever, the cache is hit most of the time, everyone’s happy. I still want to try out Azure DNS someday, maybe even Static Web Apps once they move out of Preview and into GA.

But until then, I’m happy with my setup. It’s a good setup, no need to tweak it for a while. Better get back to writing more ml articles.

By the way, if you’ve enjoyed this article you might want to read the others, too. I’ll let you know as soon as I write the next one, just make sure to subscribe below.

Or maybe you’d like to show the Twitter thread some love?


  1. Why would it need read-write to all my repositories, public and private, when the most I’ll use it for is a public repo? Why does it need access to all my workflows? Spider sense…tingling. ↩︎