Cloudflare CDN in Practice: Cache Static Assets, Speed Up APIs, and Keep Content Fresh
A CDN only helps if you control what gets cached, for how long, and how to invalidate it safely. Cloudflare can cache your static assets automatically, but modern web apps also need:
- Predictable caching for JS/CSS/images (long-lived, immutable)
- Safe caching for HTML (short-lived, avoid personalized pages)
- Optional caching for API responses (fast reads, controlled staleness)
- Fast cache purges when you deploy or update content
This hands-on guide walks through each piece with copy/paste examples you can run today.
1) Verify what’s happening: CF-Cache-Status and friends
Before changing config, measure. Cloudflare adds the CF-Cache-Status response header to indicate whether the response was served from cache or fetched from your origin. :contentReference[oaicite:0]{index=0}
Run this against a real asset (like /assets/app.css):
curl -I https://your-domain.com/assets/app.css
Look for headers like:
CF-Cache-Status: HIT(served from Cloudflare cache)CF-Cache-Status: MISS(not cached at that edge yet; fetched from origin)CF-Cache-Status: BYPASS(Cloudflare was told not to cache)CF-Cache-Status: DYNAMIC(treated as dynamic/uncacheable)
Cloudflare documents this header as the primary signal for cache behavior. :contentReference[oaicite:1]{index=1}
Tip: Always test at least twice. The first request might be a MISS and the second a HIT if it’s cacheable.
2) Make static assets “cache forever” (the right way)
The most common win is setting long cache lifetimes for versioned files (filenames that include a hash), like:
app.8c1f3a2.jsstyles.91bd10f.css
Because the filename changes on every build, you can safely cache them for a year.
Node/Express example: immutable caching for hashed assets
If you serve assets from Express:
import express from "express"; import path from "path"; const app = express(); // Serve hashed assets with long cache app.use("/assets", express.static(path.join(process.cwd(), "public/assets"), { setHeaders(res, filePath) { // If your build outputs hashed filenames, you can mark as immutable. // Adjust the condition if you use a different naming pattern. const isHashed = /\.[0-9a-f]{8,}\./i.test(filePath); if (isHashed) { res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } else { // Non-hashed assets: cache shorter to avoid stale files. res.setHeader("Cache-Control", "public, max-age=300"); } } })); app.get("/", (req, res) => { res.sendFile(path.join(process.cwd(), "public/index.html")); }); app.listen(3000, () => console.log("Listening on :3000"));
Why it matters: Cloudflare respects origin cache headers in many cases. If your origin marks assets as no-store or private, you’ll often see BYPASS or DYNAMIC instead of HIT. :contentReference[oaicite:2]{index=2}
Nginx example: long cache for /assets/
server { listen 80; server_name your-domain.com; root /var/www/public; location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } location / { # HTML: keep short unless you have a specific strategy add_header Cache-Control "public, max-age=60"; try_files $uri /index.html; } }
3) Use Cloudflare Cache Rules (instead of “Page Rules” hacks)
In Cloudflare, “Cache Rules” let you decide if certain requests are eligible for cache or should bypass cache, based on conditions like path, hostname, file extension, headers, etc. :contentReference[oaicite:3]{index=3}
A practical setup for many apps:
- Eligible for cache:
/assets/*,*.css,*.js,*.png,*.jpg,*.webp,*.svg,*.woff2 - Bypass cache:
/account/*,/checkout/*,/api/auth/*(anything personalized) - Careful with HTML: cache only if you know it’s not personalized, or keep TTL short
Junior-dev rule of thumb: If the response changes per user (cookies, auth headers, A/B experiments), don’t cache it at the CDN unless you really understand the consequences.
4) Cache an API endpoint safely with a Cloudflare Worker (Cache API)
Sometimes you want to cache a “public” API response (think: product catalog, public blog feed, exchange rates) for 30–120 seconds to reduce origin load and speed up responses globally.
Cloudflare Workers provide a Cache API (caches.default) so you can implement edge caching logic in code. :contentReference[oaicite:4]{index=4}
Here’s a Worker that:
- Only caches
GETrequests - Ignores “tracking” query parameters you don’t want in the cache key
- Caches for 60 seconds
- Returns cached content when available
export default { async fetch(request, env, ctx) { if (request.method !== "GET") { return fetch(request); } const url = new URL(request.url); // Only cache a specific public endpoint if (!url.pathname.startsWith("/api/public/catalog")) { return fetch(request); } // Normalize cache key: drop noisy params like utm_* for (const [key] of url.searchParams) { if (key.startsWith("utm_") || key === "fbclid") { url.searchParams.delete(key); } } const cacheKey = new Request(url.toString(), request); const cache = caches.default; let response = await cache.match(cacheKey); if (response) { // Optional: expose a header so you can see cache hits in DevTools response = new Response(response.body, response); response.headers.set("X-Edge-Cache", "HIT"); return response; } // Cache miss: fetch from origin response = await fetch(request); // Only cache successful JSON responses const contentType = response.headers.get("Content-Type") || ""; if (response.ok && contentType.includes("application/json")) { // Clone so we can put one copy in cache and return the other const toCache = new Response(response.body, response); // Set edge/browser caching semantics toCache.headers.set("Cache-Control", "public, max-age=60"); ctx.waitUntil(cache.put(cacheKey, toCache)); } const out = new Response(response.body, response); out.headers.set("X-Edge-Cache", "MISS"); return out; } };
Important nuance: The Workers Cache API behaves like data-center–local cache (the first request in a new region may still be a miss). That’s expected when you rely on caches.default. :contentReference[oaicite:5]{index=5}
5) Purge cache on deploy (single URL, tags, prefixes)
Eventually you’ll ship a change and need the world to see it now. Cloudflare supports multiple purge methods, including purging by URL and more advanced “flex purge” approaches (tags, hosts, prefixes). :contentReference[oaicite:6]{index=6}
Here’s a small Python script to purge specific URLs after a deploy (works nicely in CI):
import os import requests CF_API_TOKEN = os.environ["CF_API_TOKEN"] CF_ZONE_ID = os.environ["CF_ZONE_ID"] URLS_TO_PURGE = [ "https://your-domain.com/", "https://your-domain.com/index.html", "https://your-domain.com/assets/app.8c1f3a2.js", ] def purge(urls): endpoint = f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/purge_cache" headers = { "Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json", } payload = {"files": urls} r = requests.post(endpoint, headers=headers, json=payload, timeout=20) r.raise_for_status() data = r.json() if not data.get("success"): raise RuntimeError(f"Purge failed: {data}") print("Purge OK:", data.get("result")) if __name__ == "__main__": purge(URLS_TO_PURGE)
If your app emits “cache tags” (often done by frameworks or reverse proxies), you can also purge by tags for a whole group of related resources. :contentReference[oaicite:7]{index=7}
6) Debug checklist: why am I not getting HIT?
- Origin sends uncacheable headers: If your origin returns
Cache-Control: private,no-cache, ormax-age=0, Cloudflare may bypass cache. :contentReference[oaicite:8]{index=8} - You’re caching HTML that varies per user: A cookie or auth header can turn a response “dynamic.” Split personalized routes from public routes.
- Query strings explode your cache: If every request has unique params (
?t=timestamp), each becomes a separate cache key. Normalize or drop noisy params (Worker example above). - Cache Rules bypass your path: One broad bypass rule can accidentally include your static assets. Double-check rule order and conditions. :contentReference[oaicite:9]{index=9}
- You tested only once: First request may be
MISS; retry to confirm aHIT.
Wrap-up: a simple, reliable CDN strategy
If you’re implementing Cloudflare caching for a typical web app, this is a solid baseline:
- Hashed assets:
Cache-Control: public, max-age=31536000, immutable - HTML: short TTL (or cache selectively), avoid caching personalized routes
- APIs: cache only truly public responses; use a Worker for safe edge caching
- Deploys: purge URLs (or tags/prefixes) automatically in CI
- Debugging: use
CF-Cache-Status+ repeat requests to confirm behavior
Once you’ve done this once, you’ll stop guessing and start treating caching as part of your application design—just like database indexes or API timeouts.
::contentReference[oaicite:10]{index=10}
Leave a Reply