Cloudflare CDN in Practice: Cache Static Assets Safely Without Breaking Deploys

Cloudflare CDN in Practice: Cache Static Assets Safely Without Breaking Deploys

Cloudflare CDN can make a small web app feel much faster, but bad caching rules can also serve stale JavaScript, old CSS, or even private HTML. The practical goal is simple: cache files that are safe to cache for a long time, keep dynamic pages fresh, and purge only what changed during deployment.

In this guide, we will configure a typical web app with hashed assets such as app.8f3a2c.js, a small Node/Express example, and a deploy script that purges Cloudflare by URL. Cloudflare’s current cache docs recommend Cache Rules for cache configuration, and single-file purge by URL is the recommended purge method for targeted updates. :contentReference[oaicite:0]{index=0}

What Cloudflare Should and Should Not Cache

Cloudflare caches many static file types by default, but HTML and other dynamic content are not cached by default. You can cache HTML with rules, but you should only do that when you fully understand login state, cookies, personalization, and purge behavior. :contentReference[oaicite:1]{index=1}

  • Good long-term cache candidates: versioned JavaScript, CSS, fonts, images, and SVG files.
  • Usually short-term cache candidates: public JSON files, RSS feeds, sitemap files, and generated metadata.
  • Usually do not cache at the edge: dashboards, account pages, carts, checkout pages, admin pages, and personalized HTML.

The safest production pattern is to give static assets immutable filenames and long cache headers. When the file content changes, the filename changes too. That means browsers and Cloudflare can cache aggressively without serving stale code.

Step 1: Serve Hashed Static Assets

Most modern build tools already generate hashed filenames. For example, Vite, Webpack, Rollup, and Angular builds often output files like:

dist/ index.html assets/ app.8f3a2c.js styles.91ab44.css logo.aa12ff.svg

The important detail is that index.html should not be cached for a year, because it points to the current asset filenames. The hashed assets can be cached for a year because changing the content creates a new URL.

Step 2: Add Cache-Control Headers in Express

Here is a minimal Express app that serves HTML carefully and assets aggressively:

import express from "express"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const distPath = path.join(__dirname, "dist"); app.use( "/assets", express.static(path.join(distPath, "assets"), { immutable: true, maxAge: "1y", setHeaders(res) { res.setHeader( "Cache-Control", "public, max-age=31536000, immutable" ); } }) ); app.get("/", (req, res) => { res.setHeader("Cache-Control", "no-cache"); res.sendFile(path.join(distPath, "index.html")); }); app.get("/health", (req, res) => { res.setHeader("Cache-Control", "no-store"); res.json({ ok: true }); }); app.listen(3000, () => { console.log("App running on http://localhost:3000"); });

This setup gives three different caching behaviors:

  • /assets/* can be cached for one year because filenames are versioned.
  • / uses no-cache, which allows revalidation before reuse.
  • /health uses no-store, because monitoring endpoints should always be fresh.

Cloudflare can respect origin cache headers unless you override them with Cache Rules or Edge Cache TTL settings. Use origin headers as your first layer, then add Cloudflare rules only where they make behavior clearer. :contentReference[oaicite:2]{index=2}

Step 3: Verify Headers Locally

Before touching Cloudflare, check your app responses with curl:

curl -I http://localhost:3000/ HTTP/1.1 200 OK Cache-Control: no-cache
curl -I http://localhost:3000/assets/app.8f3a2c.js HTTP/1.1 200 OK Cache-Control: public, max-age=31536000, immutable

After deploying behind Cloudflare, check the public URL:

curl -I https://example.com/assets/app.8f3a2c.js

Look for headers such as cache-control and cf-cache-status. The exact value of cf-cache-status can vary, but common values include HIT, MISS, DYNAMIC, and EXPIRED. A first request is often a miss; a repeated request may become a hit.

Step 4: Create a Cloudflare Cache Rule for Assets

For many apps, origin headers are enough. Still, a Cloudflare Cache Rule can make the policy explicit. In the Cloudflare dashboard, create a rule that matches static assets:

Hostname equals "example.com" AND URI Path starts with "/assets/"

Then configure:

  • Cache eligibility: eligible for cache
  • Edge TTL: one month or longer
  • Browser TTL: respect origin headers, or set a matching long TTL

Avoid a broad “cache everything” rule for the entire site unless your pages are fully public and you have a clear purge strategy. Cloudflare warns that caching dynamic content can expose unintended information if applied carelessly. :contentReference[oaicite:3]{index=3}

Step 5: Purge Changed Files During Deploy

If you use hashed filenames, old files do not need to be purged immediately. New HTML points to new assets. However, you may still want to purge index.html, sitemap.xml, or a few updated public JSON files after deployment.

Create a small purge script:

// scripts/purge-cloudflare.js const zoneId = process.env.CLOUDFLARE_ZONE_ID; const apiToken = process.env.CLOUDFLARE_API_TOKEN; const baseUrl = process.env.PUBLIC_BASE_URL; if (!zoneId || !apiToken || !baseUrl) { console.error("Missing CLOUDFLARE_ZONE_ID, CLOUDFLARE_API_TOKEN, or PUBLIC_BASE_URL"); process.exit(1); } const files = [ `${baseUrl}/`, `${baseUrl}/sitemap.xml`, `${baseUrl}/robots.txt` ]; const response = await fetch( `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, { method: "POST", headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ files }) } ); const result = await response.json(); if (!response.ok || !result.success) { console.error("Cloudflare purge failed:"); console.error(JSON.stringify(result, null, 2)); process.exit(1); } console.log("Purged Cloudflare cache:"); for (const file of files) { console.log(`- ${file}`); }

Run it after deployment:

CLOUDFLARE_ZONE_ID="your-zone-id" \ CLOUDFLARE_API_TOKEN="your-api-token" \ PUBLIC_BASE_URL="https://example.com" \ node scripts/purge-cloudflare.js

The Cloudflare API supports purging cached content by URL, and that targeted approach is safer than purging the entire zone after every deploy. :contentReference[oaicite:4]{index=4}

Step 6: Add the Purge to GitHub Actions

Here is a simple CI/CD example. The deploy command is intentionally generic; replace it with your host’s deployment command.

name: Deploy on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node uses: actions/setup-node@v4 with: node-version: 22 - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Deploy app run: npm run deploy - name: Purge Cloudflare cache run: node scripts/purge-cloudflare.js env: CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} PUBLIC_BASE_URL: https://example.com

Create a Cloudflare API token with the minimum permissions needed for cache purge on the target zone. Do not use a global API key in CI unless you have no alternative.

Common Mistakes to Avoid

  • Caching HTML for logged-in users: this can leak account-specific pages or show the wrong state.
  • Using long cache headers on unversioned files: /app.js with a one-year cache is painful because the URL never changes.
  • Purging everything on every deploy: this removes useful cache entries and increases origin traffic.
  • Ignoring local browser cache: when debugging, test with curl -I or a clean browser profile.
  • Forgetting non-HTML public files: remember sitemap.xml, robots.txt, and generated feeds.

A Practical CDN Checklist

  • Use hashed filenames for JavaScript, CSS, and image bundles.
  • Set Cache-Control: public, max-age=31536000, immutable on hashed assets.
  • Set Cache-Control: no-cache on HTML entry points.
  • Keep authenticated pages out of aggressive CDN caching.
  • Create narrow Cloudflare Cache Rules instead of broad “cache everything” rules.
  • Purge only URLs that must update immediately after deployment.

Conclusion

A good Cloudflare CDN setup is not just “turn caching on.” It is a contract between your build system, your HTTP headers, and your deploy pipeline. Version your assets, cache them aggressively, keep HTML easy to refresh, and purge only the small set of URLs that need immediate updates. This gives junior and mid-level teams most of the performance benefit without creating hard-to-debug stale deploys or unsafe cached pages.

::contentReference[oaicite:5]{index=5}


Leave a Reply

Your email address will not be published. Required fields are marked *