Netlify is a great service with a fast CDN, but if you want to proxy your site through Cloudflare and set up a cache and optimizations there you can do that too. I usually achieve faster speeds with the CF cache enabled, so if you need more speed you should try it.
But doubling up has its drawbacks. On a deploy CF still serves the cached files to the browser even when they changed in the meantime.
Why the old files stick around
Netlify sends cache-control: public, max-age=0, must-revalidate with everything it serves. That tells every cache between the browser and Netlify to check back before using what it has, which is why a deploy on Netlify shows up right away. Cloudflare plays by that rule, and out of the box it doesn't cache HTML anyway, only files it knows by extension. So with Cloudflare in front of Netlify and nothing else you get the DNS and the proxy, but not much of a cache.
The usual fix is a Page Rule on your domain with Cache Level: Cache Everything and an Edge Cache TTL. The TTL overrides whatever Netlify says, Cloudflare keeps your pages at the edge for a day or a month, and the site gets fast. It also gets stuck. Push a fix for a typo, watch the deploy go green on Netlify, reload: the typo is still there. Private window, still there. That's not a bug, that's Cloudflare doing exactly what I told it to.
The chunks of a Nuxt build are fine, their names carry a hash and a new build means new URLs. The pages, the feed, the sitemap and the images keep their URLs, and those are what you see stale. The stale page even points at the chunks of the previous build, which Netlify deleted on deploy. In my post on missing chunks I listed doubling up on the CDN as a way to keep old chunks alive for a while. Here the same cache works against me.
What should I do then
- Wait. The Edge Cache TTL runs out eventually. A day is a long time to look at a typo.
- Purge by hand. Caching, Configuration, Purge Everything on the Cloudflare dashboard. Two clicks, and I forget them every second deploy.
- Development Mode. It bypasses the cache for three hours. Also a dashboard click, and it switches off the very thing you came to Cloudflare for.
- Automate it. Netlify knows when a deploy is done and Cloudflare has an API for purging. Let's connect the two.
A token for Cloudflare
Cloudflare has scoped API tokens, so you don't have to hand your Global API Key to anything. Go to My Profile, API Tokens, Create Token, and pick a custom token.
You also need the Zone ID, it's in the right column of the zone's Overview page. With both in hand, check that the token works before you build anything on it:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID/purge_cache" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
If the answer has "success": true in it, the cache is gone and the token is good.
Then put both values on Netlify under Site settings, Build & deploy, Environment, as CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN. The build reads them from process.env and they never touch the repo.
The timing problem
The obvious place for the purge is the end of the build command: nuxt generate && curl .... Netlify Build Plugins had the same limit until recently, their last event was onPostBuild, which runs after the build and before the upload. Both purge too early. Between the purge and the moment the new files are live, Cloudflare happily fetches the old pages from Netlify again and keeps them for another day. It's a short window, but one visitor is enough. The purge has to wait until the deploy is live.
At the end of January Netlify added two post-deploy events to Build Plugins: onSuccess runs after a successful deploy, onEnd at the very end, success or not. A plugin can't fail a build that's already live, but it can run code at exactly the right moment, and that's all we need.
The ready-made plugin
There is one already: netlify-purge-cloudflare-on-deploy by Chris Muktar. It reads the two variables above, so the setup is two lines:
# netlify.toml
[[plugins]]
package = "netlify-purge-cloudflare-on-deploy"
Its readme still says it purges before the deploy. That was true until last month, the code runs in onSuccess, so with the new events it purges when the site is live. If you don't care about the details, install it and you're done.
Rolling my own
I like to know what runs in my build, and this is small enough to keep in the repo as a local plugin. A plugin is a folder with a manifest and an index.js:
# plugins/purge-cloudflare/manifest.yml
name: purge-cloudflare
// plugins/purge-cloudflare/index.js
const fetch = require("node-fetch");
module.exports = {
// onSuccess runs after the deploy is live, so the purge can't race the upload
async onSuccess({ utils }) {
// Deploy previews and branch deploys live on netlify.app, Cloudflare never sees them
if (process.env.CONTEXT !== "production") {
return utils.status.show({
summary: "Not a production deploy, Cloudflare cache kept.",
});
}
const { CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_TOKEN } = process.env;
const response = await fetch(
`https://api.cloudflare.com/client/v4/zones/${CLOUDFLARE_ZONE_ID}/purge_cache`,
{
method: "POST",
headers: {
Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ purge_everything: true }),
},
);
const result = await response.json();
if (!result.success) {
// The site is already live, so this can only mark the plugin as failed in the deploy summary
return utils.build.failPlugin(
`Cloudflare purge failed: ${JSON.stringify(result.errors)}`,
);
}
// Shows up in the deploy summary on Netlify
utils.status.show({ summary: "Cloudflare cache purged." });
},
};
Then point netlify.toml at the folder instead of the package:
# netlify.toml
[[plugins]]
package = "./plugins/purge-cloudflare"
node-fetch comes from the site's node_modules, so add it to your dev dependencies if it isn't there yet. The CONTEXT variable is set by Netlify on every build, so a deploy preview doesn't flush the cache of the live site for nothing.
The other way: a function
Before plugins got these events the way to do this was an event-triggered function. Name a Netlify Function deploy-succeeded and Netlify calls it when a deploy is live, no webhook needed. It still works, and Frank Lin wrote up a variant with an outgoing webhook. I prefer the plugin because the result shows up in the deploy summary next to the build, not in a separate functions log.
Does it work
Push a commit and open the deploy on Netlify. The deploy summary lists the plugin with "Cloudflare cache purged." next to it. Reload the site with the network tab open: the first response has cf-cache-status: MISS, the second one HIT. The cache is back to doing its job, with the new files this time.
Purge Everything is a blunt tool. For a blog it's fine, the next visitor warms the cache again. For a bigger site send the changed URLs in a files array instead, the API takes up to 30 at a time.
Read more here:
- Post-deploy events now available to Build Plugins on the Netlify blog
- Develop build plugins in the Netlify docs
- netlify-purge-cloudflare-on-deploy by Chris Muktar
- Purge cached content in the Cloudflare API docs
- Enable CDN (Cloudflare) caching for your Netlify site by Hugo Di Francesco
- Automatically purge Cloudflare cache after Netlify build by Frank Lin