Cloudflare CDN in Practice: Cache the Right Things, Debug Like a Pro, and Purge Safely
Putting Cloudflare in front of your app can be a huge win: static assets load faster worldwide, your origin gets fewer requests, and you get a bunch of security features “for free.” The part that trips teams up is caching: what gets cached, where, for how long, and how to invalidate it when you ship changes.
This guide is hands-on: you’ll learn how to (1) make your origin cache-friendly, (2) verify what Cloudflare is doing with real headers, (3) selectively bypass cache (cookies, auth), (4) add edge caching with a Worker when you need it, and (5) purge by URL without nuking everything.
1) Start at the origin: set cache headers intentionally
Cloudflare generally respects standard HTTP caching headers. Before you touch Cloudflare settings, make sure your origin sends sane headers for different kinds of content:
- Immutable assets (hashed files like
app.9c1a2f.js): long cache, immutable. - Non-hashed assets (like
/logo.png): shorter cache unless you version it. - HTML: often dynamic; cache carefully (or not at all) unless you know it’s safe.
- API responses: frequently user-specific; usually
no-storeor short cache withVary.
Nginx example (assets cached aggressively, HTML not cached):
# /etc/nginx/conf.d/site.conf server { listen 80; server_name example.com; root /var/www/html; # Hashed static assets: cache for 1 year, immutable location ~* \.(?:css|js|woff2|png|jpg|jpeg|gif|svg)$ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # HTML: don't cache by default location / { add_header Cache-Control "no-store"; try_files $uri /index.html; } }
Express example (API not cached, assets cached):
import express from "express"; import path from "path"; const app = express(); app.get("/api/me", (req, res) => { // User-specific response res.set("Cache-Control", "no-store"); res.json({ id: "123", name: "Ada" }); }); app.use("/assets", express.static(path.join(process.cwd(), "assets"), { setHeaders(res, filePath) { // If you serve hashed assets from /assets, this is safe: res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } })); app.listen(3000);
Why this matters: if your origin accidentally sends Cache-Control: private, no-cache, or max-age=0, Cloudflare may bypass caching for that response. (You’ll often see cf-cache-status: BYPASS.) :contentReference[oaicite:0]{index=0}
2) Verify caching with headers (don’t guess)
Use curl and look at Cloudflare response headers. The most useful one is cf-cache-status, which tells you whether Cloudflare served the response from cache.
Common values include HIT, MISS, EXPIRED, STALE, and others. :contentReference[oaicite:1]{index=1}
# First request: likely MISS (Cloudflare fetches from origin and may cache) curl -I https://yourdomain.com/assets/app.9c1a2f.js # Second request: should become HIT if cacheable and popular enough curl -I https://yourdomain.com/assets/app.9c1a2f.js
You’ll see something like:
cf-cache-status: HIT age: 1234
Debug tip: also check cf-ray to confirm which edge data center served your request (handy when comparing regions). :contentReference[oaicite:2]{index=2}
3) Use Cache Rules to bypass cache for “risky” requests
Cloudflare’s Cache Rules let you define when to cache and when to bypass. This is how you avoid caching personalized pages (cookies, auth) while still caching static pages or assets.
A typical “safe default” setup:
- Bypass cache when a session/auth cookie is present.
- Bypass cache for paths like
/api/*,/admin/*. - Cache everything for static assets paths (
/assets/*) if your origin headers aren’t perfect.
Cloudflare explicitly supports “Bypass cache” as an option in Cache Rules. :contentReference[oaicite:3]{index=3}
Practical pattern: bypass for auth cookie, cache for public content.
- If request has cookie
session(or your auth cookie name) → bypass. - Else if path starts with
/assets/→ cache.
This avoids the classic nightmare where one user’s personalized HTML gets cached and served to someone else.
4) Edge caching for APIs with a Cloudflare Worker (when headers aren’t enough)
Sometimes you want caching, but only for a subset of API responses (e.g., a public “top posts” endpoint) and you want Cloudflare to cache it at the edge even if the origin isn’t perfectly configured.
Cloudflare Workers can control caching behavior by passing a cf object to fetch() (TTL, caching behavior, etc.). Cloudflare’s “cache using fetch” example shows this pattern. :contentReference[oaicite:4]{index=4}
Example: cache a public JSON endpoint for 60 seconds at the edge
export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Only cache this specific endpoint if (url.pathname === "/api/public/top-posts") { const originUrl = "https://origin.example.com" + url.pathname; const res = await fetch(originUrl, { cf: { // Cache at the edge for 60 seconds cacheTtl: 60, // Force caching even if origin headers are conservative cacheEverything: true, }, headers: { // Forward only what you need; avoid cookies for public cache "Accept": "application/json", }, }); // You can also add browser caching separately if you want const newHeaders = new Headers(res.headers); newHeaders.set("Cache-Control", "public, max-age=60"); return new Response(res.body, { status: res.status, headers: newHeaders, }); } // Everything else passes through untouched return fetch(request); }, };
Notes juniors often miss:
- Don’t forward cookies into a shared cache unless you really mean it.
- Edge cache TTL (
cf.cacheTtl) and browser cache headers (Cache-Control) are related but not the same thing. - If you later need selective invalidation, design predictable URLs (or versioned paths) so purging is easy.
5) Purge cache by URL (ship changes without “purge everything”)
When you deploy a change, you usually don’t want to purge your entire zone. Purge specific URLs instead—especially for HTML pages or non-hashed assets.
Cloudflare’s API supports purging cached content by URL. :contentReference[oaicite:5]{index=5}
Example: purge a single file by URL
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://yourdomain.com/logo.png", "https://yourdomain.com/index.html" ] }'
Practical strategy:
- If you use hashed filenames for assets, you usually don’t need to purge them (new deploy = new filename).
- Purge only “stable URL” content:
/index.html,/logo.png, landing pages, sitemap, etc. - For blogs/CMS: purge the updated post URL + homepage + category pages that list it.
6) A simple debugging checklist for “why isn’t this caching?”
- Check
cf-cache-status: Is itMISS(first request) or alwaysBYPASS/DYNAMIC? :contentReference[oaicite:6]{index=6} - Inspect origin headers: Are you sending
Cache-Control: private,no-cache,no-store, ormax-age=0? Those commonly trigger bypass behavior. :contentReference[oaicite:7]{index=7} - Is the response personalized? If cookies or auth are involved, it should probably not be cached.
- Is it even eligible? By default, many HTML pages are treated as dynamic unless you explicitly cache them with rules. :contentReference[oaicite:8]{index=8}
- Try a second request: A single request only tells you so much. Repeat and see if it becomes
HIT. - Confirm URL variance: Query strings can create separate cache entries unless you normalize them with rules.
7) Recommended “starter” configuration (safe for most apps)
- Assets: serve hashed files; set
Cache-Control: public, max-age=31536000, immutable. - HTML: default
no-storeunless you’re intentionally doing full-page caching. - API: default
no-store, then opt-in caching for specific public endpoints (possibly with a Worker). - Cloudflare Cache Rules:
- Bypass when auth/session cookies exist. :contentReference[oaicite:9]{index=9}
- Bypass for
/api/*,/admin/*. - Cache static paths (
/assets/*) if needed.
- Purge: purge by URL on deploy for non-hashed URLs; avoid “purge everything” unless you’re firefighting.
Once you get comfortable reading cf-cache-status and shaping caching from your origin outward, Cloudflare becomes predictable—and you can confidently speed up real-world apps without serving stale or incorrect content.
::contentReference[oaicite:10]{index=10}
Leave a Reply