Cloudflare CDN in Practice: Cache Smarter, Ship Faster, and Debug Like a Pro

Cloudflare CDN in Practice: Cache Smarter, Ship Faster, and Debug Like a Pro

Cloudflare can make a site feel “instantly faster” — but only if you understand what’s actually being cached, when it’s bypassed, and how headers and rules interact. This hands-on guide shows a practical setup for junior/mid developers: caching static assets aggressively, keeping HTML fresh, adding edge logic with a Worker, and debugging cache behavior with confidence.

We’ll focus on a typical web app setup:

  • HTML is dynamic (SSR or templates) and should usually be no-cache or short-lived
  • Static assets (JS/CSS/images/fonts) should be cached long and versioned
  • Some API responses can be cached carefully (public, read-only)

1) The mental model: “Browser cache” vs “Edge cache” vs “Origin”

When a request comes in, there are multiple layers:

  • Browser cache (controlled by Cache-Control, ETag, etc.)
  • Cloudflare edge cache (Cloudflare caches responses at the edge)
  • Your origin (your server: Nginx/Apache/app)

Cloudflare will generally respect your origin’s caching headers for many asset types, but you can also override caching behavior with Cloudflare rules (useful, but easy to misconfigure). The goal is predictable caching:

  • Assets: Cache-Control: public, max-age=31536000, immutable
  • HTML: Cache-Control: no-cache (or short max-age + revalidation)

2) Cache static assets the right way (the “immutable + versioned” pattern)

To safely cache assets for a year, you must version them (usually by hashing the filename):

  • app.3f2a9c1.js
  • styles.91b7d2a.css

Then you can confidently ship this header from your origin for assets only:

# Example (Nginx) for hashed static assets location ~* ^/assets/.*\.(js|css|png|jpg|jpeg|gif|svg|webp|woff2)$ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; }

If you’re on Node/Express, you can do something similar:

import express from "express"; import path from "path"; const app = express(); app.use("/assets", express.static(path.join(process.cwd(), "assets"), { setHeaders(res) { res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } })); app.listen(3000);

Why this works: If the content changes, the filename changes, so caches never serve a stale file.

3) Keep HTML fresh (and avoid caching logged-in pages)

For HTML, especially personalized pages, you usually want Cloudflare to pass through and let the browser revalidate:

# Nginx example for HTML location / { add_header Cache-Control "no-cache"; proxy_pass http://app_upstream; }

In many apps, a safe baseline is:

  • Static assets: long cache, immutable
  • HTML: no-cache (so the browser revalidates)
  • Authenticated content: never edge-cache unless you really know what you’re doing

If you do cache HTML at the edge, do it intentionally and keep it short-lived, and vary correctly (more on that below).

4) Add a “sane defaults” Cache Rule strategy

Cloudflare’s dashboard lets you define cache behavior with rules (often called Cache Rules). A practical approach:

  • Rule A: if path starts with /assets/ → “Cache” with long Edge TTL
  • Rule B: if path equals / or ends with .html → “Bypass cache” (or very short)
  • Rule C: if path starts with /api/ → “Bypass cache” unless explicitly cacheable

Even if you rely on origin headers, rules help prevent accidental caching of dynamic pages.

5) Cache an API endpoint safely (public, read-only)

Sometimes you have a public endpoint like /api/public/products that changes a few times per hour. You can add explicit caching headers from your app:

// Express example: cache for 60 seconds at browser + CDN app.get("/api/public/products", async (req, res) => { const products = await loadProducts(); res.setHeader("Cache-Control", "public, max-age=60, s-maxage=300"); // max-age=60 (browser), s-maxage=300 (shared caches like CDN) res.json(products); });

s-maxage is the key: it tells shared caches (CDNs) how long they can keep it, often longer than the browser.

Gotcha: Don’t cache responses that depend on cookies or authorization headers unless you’re varying correctly and understand the security implications.

6) Vary correctly: avoid “one user sees another user’s data”

If a response changes based on headers, you must tell caches via Vary. Common cases:

  • Localization: Vary: Accept-Language
  • Compression: Vary: Accept-Encoding (often handled automatically)
  • Auth: generally don’t cache responses that vary by auth

Example for localized public content:

res.setHeader("Cache-Control", "public, max-age=60, s-maxage=300"); res.setHeader("Vary", "Accept-Language"); 

7) Use a Worker to set headers (when you can’t change the origin)

Sometimes you can’t modify origin headers (legacy app, third-party hosting). A Cloudflare Worker can adjust caching behavior at the edge. Here’s a simple Worker that:

  • Sets long cache headers for /assets/
  • Forces no-cache for HTML
export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Fetch from origin (or other Worker chain) const response = await fetch(request); // Clone to modify headers const newResponse = new Response(response.body, response); if (url.pathname.startsWith("/assets/")) { newResponse.headers.set("Cache-Control", "public, max-age=31536000, immutable"); } else if (request.headers.get("Accept")?.includes("text/html")) { newResponse.headers.set("Cache-Control", "no-cache"); } return newResponse; } };

Tip: A Worker can also normalize URLs, redirect, rewrite, or add security headers. But keep Workers small and test carefully — they sit on the hot path for every request they handle.

8) Debugging: prove what’s cached (with curl)

Don’t guess. Check response headers from Cloudflare. A practical workflow:

  • Request an asset twice and compare headers
  • Look for Cloudflare cache indicators and your Cache-Control
# First request: likely MISS curl -I https://example.com/assets/app.3f2a9c1.js # Second request: should become HIT if caching is configured curl -I https://example.com/assets/app.3f2a9c1.js

What you’re looking for:

  • Cache-Control matches your intention
  • A Cloudflare cache status header indicates HIT/MISS (exact header names can vary by plan/features)
  • Age increasing can indicate caching

If HTML is unexpectedly cached, inspect:

  • Does your origin send Cache-Control: public for HTML?
  • Do you have a Cache Rule overriding behavior?
  • Are you using “Cache Everything” somewhere (common source of surprises)?

9) Cache busting and deploys: the practical checklist

Here’s a robust deploy-friendly pattern:

  • Build assets with hashed filenames (app.<hash>.js)
  • Set immutable caching for hashed assets
  • Keep HTML no-cache so it always picks up new asset references
  • Only purge cache when you must (hashed assets usually remove the need)

If you can’t hash filenames (not ideal), you’ll end up purging frequently. In that case, consider:

  • Shorter TTLs for assets
  • Querystring versioning (app.js?v=123) as a fallback

10) Quick “best effort” security headers at the edge

While you’re in the neighborhood, add a few safe headers (origin or Worker). Example Worker snippet:

newResponse.headers.set("X-Content-Type-Options", "nosniff"); newResponse.headers.set("X-Frame-Options", "SAMEORIGIN"); newResponse.headers.set("Referrer-Policy", "strict-origin-when-cross-origin"); newResponse.headers.set("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); 

Note: For Content-Security-Policy, start in report-only mode first; CSP can break sites if rolled out blindly.

11) A minimal “done right” recipe

If you want a straightforward, low-drama Cloudflare setup for a typical web app, this is a great baseline:

  • Assets under /assets/:
    • Hashed filenames
    • Cache-Control: public, max-age=31536000, immutable
    • Optional: Cloudflare Cache Rule to enforce long edge TTL
  • HTML:
    • Cache-Control: no-cache
    • Bypass edge caching for logged-in pages
  • Public API (only when safe):
    • Cache-Control: public, max-age=60, s-maxage=300
    • Vary as needed (e.g., language)
  • Debug:
    • Use curl -I to verify headers and cache behavior
    • Watch for accidental “cache everything” rules

Once you have this baseline, you can get fancy (image optimization, partial caching, edge-rendered responses), but you’ll already have the most important thing: predictable behavior that speeds up real users without breaking correctness.


Leave a Reply

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