Cloudflare CDN in Practice: Cache Rules, Purge Strategies, and Debugging for Real Web Apps

Cloudflare CDN in Practice: Cache Rules, Purge Strategies, and Debugging for Real Web Apps

Cloudflare can feel “set it and forget it”… until a CSS change won’t show up, an API starts caching unexpectedly, or performance is still meh. This hands-on guide shows a practical setup for junior/mid web developers: what to cache, what to never cache, how to purge safely, and how to debug cache behavior. We’ll use a simple app pattern: static assets (/assets), HTML pages, and an API (/api).

Goal: serve static files from edge cache, keep HTML reasonably fresh, and ensure APIs are always correct.

1) Decide Your Caching Policy (Before Clicking Buttons)

A CDN works best when your URLs make caching safe. A practical split:

  • Static assets (JS/CSS/images/fonts): cache aggressively (days/months) and use versioned filenames.
  • HTML: short cache or “cache but revalidate” (depends on your stack).
  • API responses: typically don’t cache unless explicitly designed for it.

The single biggest “unlock” is cache-busting for assets: instead of app.css, ship app.3f2a1c.css (hash in filename). When the content changes, the URL changes, and you don’t need risky full-site purges.

2) Set Correct Cache Headers at Your Origin

Cloudflare respects origin headers in many configurations, and even when you override with rules, having correct headers helps other caches (browsers, intermediate proxies) behave.

Static assets (hashed):

  • Cache-Control: public, max-age=31536000, immutable

HTML pages:

  • Common safe option: Cache-Control: no-cache (allows storing but forces revalidation)
  • Or short TTL: Cache-Control: public, max-age=60 for semi-static pages

API:

  • Usually: Cache-Control: no-store (don’t store anywhere)

Example: Node/Express origin that serves hashed assets and disables caching for the API:

import express from "express"; import path from "path"; const app = express(); // Serve versioned assets with long cache app.use("/assets", express.static(path.join(process.cwd(), "public/assets"), { setHeaders(res) { res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } })); // HTML (keep it conservative) app.get("/", (req, res) => { res.setHeader("Cache-Control", "no-cache"); res.sendFile(path.join(process.cwd(), "public/index.html")); }); // API (never cache) app.get("/api/user", (req, res) => { res.setHeader("Cache-Control", "no-store"); res.json({ id: 123, name: "Ari" }); }); app.listen(3000, () => console.log("Origin running on :3000"));

If you only do one thing: hashed assets + long cache. It’s the simplest, most effective pattern.

3) Use Cloudflare Cache Rules for a Clean “Static Yes, API No” Setup

In Cloudflare, you can create rules that match URL patterns. A practical ruleset:

  • Rule A: Bypass cache for API
    • Condition: URI path starts with /api/
    • Action: Bypass cache
  • Rule B: Cache static assets aggressively
    • Condition: URI path starts with /assets/ (or extensions: .js, .css, .png, etc.)
    • Action: Cache, Edge TTL: e.g. 1 month (or longer if hashed)
  • Rule C: HTML pages (optional)
    • Condition: Path equals / or does not start with /api/ or /assets/
    • Action: cache with a short Edge TTL (like 1–5 minutes) only if your pages are safe to cache

If your HTML is personalized (cookies, auth, localization), don’t cache it blindly at the edge. You can still use Cloudflare for TLS, HTTP/2/3, compression, and asset caching.

4) Purging Without Panic: Use Tags or Versioned URLs

Purging is where teams accidentally DoS themselves or flush too much cache. Prefer:

  • Versioned asset URLs (best): deploy new files with new names → no purge required.
  • Selective purge: purge specific URLs that changed.
  • Purge by tag (advanced): purge groups of related URLs using surrogate keys / cache tags (requires origin strategy).

Here’s a small script to purge specific URLs using Cloudflare’s API. This is useful for HTML pages that are not versioned.

#!/usr/bin/env python3 import os import requests CF_API_TOKEN = os.environ["CF_API_TOKEN"] CF_ZONE_ID = os.environ["CF_ZONE_ID"] # URLs you want to purge files = [ "https://example.com/", "https://example.com/pricing", "https://example.com/docs/getting-started", ] url = f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/purge_cache" headers = { "Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json", } resp = requests.post(url, headers=headers, json={"files": files}, timeout=20) resp.raise_for_status() data = resp.json() if not data.get("success"): raise SystemExit(f"Cloudflare purge failed: {data}") print("Purged:", files)

Tip: don’t purge “everything” on every deploy. It removes the performance benefits and can spike origin load. If your assets are hashed, most deploys can skip purging entirely.

5) Debugging Cache Behavior with curl (Fast and Reliable)

When something is cached (or not), don’t guess—inspect headers. Cloudflare commonly adds CF-Cache-Status:

  • HIT: served from cache
  • MISS: fetched from origin and may store
  • BYPASS: rule/cookie/header caused cache bypass
  • REVALIDATED / EXPIRED: cache had it but checked origin

Check a static asset:

curl -I https://example.com/assets/app.3f2a1c.js

You want to see something like:

  • Cache-Control: public, max-age=31536000, immutable
  • CF-Cache-Status: HIT (after first request)

Check an API endpoint (should not cache):

curl -I https://example.com/api/user

You want:

  • Cache-Control: no-store
  • CF-Cache-Status: BYPASS (or absent)

Common pitfall: caching differs by query string. If your assets include cache-busters like ?v=123, be consistent—better to use hashed filenames instead of query strings.

6) Avoid “Accidental Personalization Caching”

The scariest bug: a CDN caches a page with user-specific data and serves it to another user. To reduce risk:

  • Do not cache pages that vary by Cookie, Authorization, or user identity.
  • For authenticated routes, set Cache-Control: private, no-store.
  • Prefer caching public, non-personal HTML (marketing pages) and all static assets.

If you must cache HTML that varies (e.g., language or device), you need a deliberate Vary strategy (like Vary: Accept-Language) and should keep variants limited. Otherwise you’ll create cache fragmentation.

7) Bonus: A “Safe Defaults” Checklist

  • Assets: hashed filenames + max-age=31536000, immutable.
  • API: Cache-Control: no-store + Cloudflare rule to bypass /api/*.
  • HTML: cache only if public; start with short TTL or no-cache.
  • Purge: purge specific URLs only when needed; avoid purge-all.
  • Debug: use curl -I and check CF-Cache-Status + Cache-Control.

8) Putting It Together: Minimal Working Example Structure

Here’s a tiny folder layout that matches the approach:

project/ public/ index.html assets/ app.3f2a1c.js app.9d1e77.css server.js

Your deploy flow:

  • Build assets → produce hashed filenames
  • Upload assets + HTML to origin
  • Do not purge assets (new names)
  • If needed, purge / and a few key HTML URLs

This is the practical “CDN sweet spot”: your cache stays hot, deploys are predictable, and you don’t accidentally cache private data.

Wrap-up

Cloudflare shines when you treat caching as a design constraint: make assets immutable via versioned URLs, keep APIs uncacheable unless explicitly intended, and only cache HTML when it’s truly public. With a small set of rules and a repeatable purge strategy, you’ll get faster page loads without the “why is it serving the old thing?” headaches.


Leave a Reply

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