Cloudflare CDN in Practice: Cache What Matters, Purge Safely, and Debug Like a Pro (Hands-On)

Cloudflare CDN in Practice: Cache What Matters, Purge Safely, and Debug Like a Pro (Hands-On)

Cloudflare can make a site feel “instant” by caching static assets at the edge (near users). But the real wins happen when you deliberately control what gets cached, how long, and how you invalidate stale content. This guide walks through a practical setup for junior/mid developers: strong cache headers, predictable asset versioning, safe purge workflows, and a small Cloudflare Worker for edge behavior.

We’ll focus on a common web app pattern:

  • Static assets (JS/CSS/images/fonts): cache aggressively
  • HTML: cache carefully (or not at all) depending on personalization
  • APIs/admin: usually bypass cache

1) Understand Cloudflare’s caching model (the 80/20)

Cloudflare decides whether to cache a response based on:

  • Response headers from your origin (especially Cache-Control)
  • Cloudflare settings/rules (e.g., “Cache Everything”, Cache Rules)
  • Request properties like method (GET/HEAD), cookies, query strings, etc.

For most apps, the safest approach is:

  • Let Cloudflare cache static assets based on Cache-Control.
  • Keep HTML either short-lived or bypassed unless you’ve planned for it.

2) Ship correct cache headers from your origin

Your origin should be explicit. If you don’t tell caches what to do, you’ll get inconsistent behavior and painful “why is this stale?” debugging.

Goal:

  • Versioned assets: Cache-Control: public, max-age=31536000, immutable
  • Unversioned HTML: Cache-Control: no-store (or very small max-age)
  • API responses: usually no-store unless you’re intentionally caching

2a) Example: Node/Express static assets + HTML

Assume you build assets to /public/assets/app.3f2c1a9c.js (hash in filename). Serve those with a long cache. Serve HTML with no-store (common for logged-in apps).

import express from "express"; import path from "path"; const app = express(); const publicDir = path.join(process.cwd(), "public"); // Cache versioned assets aggressively app.use("/assets", express.static(path.join(publicDir, "assets"), { setHeaders(res, filePath) { // Hashed filenames => safe to cache for a year res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } })); // Images/fonts could also be long-lived if versioned app.use("/static", express.static(path.join(publicDir, "static"), { setHeaders(res) { res.setHeader("Cache-Control", "public, max-age=86400"); // 1 day as a default } })); // HTML: avoid caching unless you know it's safe app.get("*", (req, res) => { res.setHeader("Cache-Control", "no-store"); res.sendFile(path.join(publicDir, "index.html")); }); app.listen(3000, () => console.log("Listening on http://localhost:3000"));

Why this works: hashed assets can be cached “forever” because a new deploy changes filenames. HTML is frequently user-specific (cookies, auth), so we avoid caching surprises.

2b) Example: Nginx headers for common asset types

If you’re serving from Nginx, you can set cache headers by file extension. If you use hashed assets, go long-lived:

server { listen 80; server_name example.com; root /var/www/app/public; # Hashed build assets location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # HTML: do not cache by default location / { add_header Cache-Control "no-store"; try_files $uri /index.html; } }

3) Use cache-friendly asset versioning (so you purge less)

If you do only one thing: hash your JS/CSS filenames. Tools like Vite/Webpack do this automatically. With hashed filenames:

  • You can safely set max-age=31536000, immutable
  • You almost never need to purge assets
  • Rollbacks are safer (old URLs still work)

If you cannot hash filenames (legacy setups), at least append a version query string (e.g., app.css?v=20260303). It’s not as strong as hashed filenames, but it’s better than nothing.

4) Purge correctly: targeted beats “purge everything”

Sometimes you must purge: maybe your HTML is cached (intentionally), or you updated an unversioned file. Cloudflare offers multiple purge strategies. Prefer:

  • Purge by URL (targeted)
  • Purge by prefix / host (if supported by your plan/features)
  • Purge everything only as a last resort (it can spike origin load)

4a) Purge by URL (curl)

You’ll need your Zone ID and an API token with permissions to purge cache for that zone.

curl -X POST "https://api.cloudflare.com/client/v4/zones/<ZONE_ID>/purge_cache" \ -H "Authorization: Bearer <API_TOKEN>" \ -H "Content-Type: application/json" \ --data '{ "files": [ "https://example.com/index.html", "https://example.com/static/logo.png" ] }'

4b) Purge by URL (Python script)

This is handy in a release pipeline (run after deploy):

import os import requests ZONE_ID = os.environ["CF_ZONE_ID"] TOKEN = os.environ["CF_API_TOKEN"] urls_to_purge = [ "https://example.com/", "https://example.com/index.html", ] resp = requests.post( f"https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/purge_cache", headers={ "Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json", }, json={"files": urls_to_purge}, timeout=20, ) data = resp.json() if not resp.ok or not data.get("success"): raise SystemExit(f"Purge failed: {data}") print("Purge ok:", data)

Tip: keep the purge list small. If you version assets, most purges will just be / or a couple of HTML routes.

5) Cache rules that won’t break your app

Cloudflare’s dashboard features evolve, but the principles don’t:

  • Bypass cache for /api/*, /admin/*, auth callbacks, and anything cookie-heavy.
  • Cache static assets by path like /assets/* and /static/*.
  • If you cache HTML, start with very short TTLs and add a safe purge/ban strategy.

A conservative rule set looks like:

  • /api/* → bypass cache
  • /admin/* → bypass cache
  • /assets/* → cache (respect origin headers)
  • /static/* → cache (respect origin headers)

6) Add an edge “switch” with a Cloudflare Worker (optional but powerful)

Workers let you run code at the edge. A practical use-case: force no-cache on authenticated requests, while letting anonymous traffic be cached (even for HTML) if you choose to do that.

The example below:

  • Bypasses caching when a session cookie exists
  • Allows caching for anonymous users on / and /blog/* for 60 seconds
  • Always caches assets long-lived
export default { async fetch(request, env, ctx) { const url = new URL(request.url); const cookie = request.headers.get("Cookie") || ""; const isAsset = url.pathname.startsWith("/assets/"); const isBlog = url.pathname === "/" || url.pathname.startsWith("/blog/"); const isAuthed = cookie.includes("session=") || cookie.includes("auth="); // Always let assets be cached heavily (origin should also send immutable) if (isAsset) { const res = await fetch(request); return new Response(res.body, { status: res.status, headers: res.headers, }); } // If the user is authenticated, bypass cache to avoid leaking personalized HTML if (isAuthed) { const res = await fetch(request, { cf: { cacheTtl: 0, cacheEverything: false } }); return new Response(res.body, { status: res.status, headers: res.headers, }); } // Anonymous: allow short caching for selected HTML routes if (isBlog && request.method === "GET") { const res = await fetch(request, { cf: { cacheTtl: 60, cacheEverything: true } }); const headers = new Headers(res.headers); headers.set("Cache-Control", "public, max-age=60"); headers.set("X-Edge-Cache", "blog-60s"); return new Response(res.body, { status: res.status, headers }); } return fetch(request); } };

Note: Caching HTML is optional. If you’re not ready, keep HTML no-store and only cache assets. That alone usually gives a huge performance boost.

7) Debugging: confirm what’s cached and why

When something is unexpectedly slow or stale, debug in a structured way:

  • Check response headers from the browser devtools or curl -I.
  • Look for Cloudflare cache status headers (commonly CF-Cache-Status like HIT/MISS/BYPASS; exact headers can vary by configuration).
  • Verify your origin is sending the intended Cache-Control.
  • Make sure you’re not accidentally varying responses by cookies or query strings.
# Inspect headers curl -I https://example.com/assets/app.3f2c1a9c.js # Helpful for HTML too curl -I https://example.com/

Common pitfalls:

  • Unversioned assets cached too long → users see old JS/CSS. Fix with hashing or shorter TTL.
  • HTML cached while personalized → users see the wrong content. Fix by bypassing on cookies/auth.
  • Cache rules too broad (“cache everything” on all paths) → APIs break. Fix with explicit bypass rules.

8) A safe “starter checklist” you can apply today

  • Enable hashed filenames for JS/CSS builds.
  • Serve hashed assets with Cache-Control: public, max-age=31536000, immutable.
  • Serve HTML with Cache-Control: no-store unless you intentionally cache it.
  • Bypass cache for /api/* and /admin/*.
  • Implement targeted purge by URL for the few pages that must be invalidated.
  • Use a Worker only if you need edge logic (auth-based caching, short TTL HTML, etc.).

Wrap-up

The “practical Cloudflare CDN setup” is mostly about being intentional: long-cache only what’s safely versioned, keep personalized routes out of cache, and build a small purge workflow for the few endpoints that need it. Once your headers and versioning are correct, Cloudflare becomes boring—in the best way: fast, stable, and predictable.


Leave a Reply

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