Cloudflare CDN in Practice: Cache What Matters, Bypass What Doesn’t (Hands-On)
Cloudflare can make a site feel “instant” by caching content close to users. But the default setup often leaves performance on the table (or worse: caches the wrong things). This guide shows a practical approach for junior/mid web devs: decide what to cache, set the right headers, create safe cache rules, and use a Worker to handle tricky cases like “cache everything except when logged in.”
We’ll assume you have:
- A domain proxied through Cloudflare (orange cloud enabled).
- An origin server (Node/Express, Nginx, Laravel, etc.).
- Some static assets (
/assets,/images) and some dynamic pages (/dashboard,/api).
1) Understand the CDN basics (and the biggest mistake)
Cloudflare caches primarily based on:
- URL (including query string, depending on settings)
- HTTP method (GET/HEAD typically cacheable)
- Response headers like
Cache-Control,Expires,ETag,Last-Modified
The biggest mistake: “Cache everything” without excluding personalized content. If you cache pages that depend on cookies (sessions, auth, A/B tests), you can leak content between users. Our strategy will be:
- Aggressively cache static assets (fingerprinted files, images, CSS/JS).
- Carefully cache public HTML (home page, marketing pages) with short TTL and safe rules.
- Never cache authenticated or user-specific responses (dashboards, account pages, most APIs).
2) Verify what Cloudflare is doing right now
Use curl to inspect Cloudflare cache headers. The most useful ones are:
cf-cache-status:HIT,MISS,REVALIDATED,BYPASS, etc.age: how long the object has been cached (seconds)
curl -I https://example.com/assets/app.9f3a1c2b.js curl -I https://example.com/ curl -I https://example.com/dashboard
Example output you might see:
cf-cache-status: HIT age: 842 cache-control: public, max-age=31536000, immutable
If you see cf-cache-status: BYPASS for assets you expect cached, you likely need better cache headers at the origin or a Cloudflare cache rule.
3) Set the right origin cache headers (the foundation)
Your origin should clearly label what is cacheable. A solid baseline:
- Static assets (hashed filenames): cache for a year, immutable.
- Public HTML: short TTL (e.g., 5 minutes) so changes roll out quickly.
- Private pages / authenticated responses:
no-store.
3.1 Example: Express.js headers
import express from "express"; import path from "path"; const app = express(); // 1) Fingerprinted assets: /assets/app.[hash].js app.use("/assets", express.static(path.join(process.cwd(), "public/assets"), { setHeaders(res, filePath) { // If you control naming and files are content-hashed, this is safe: res.setHeader("Cache-Control", "public, max-age=31536000, immutable"); } })); // 2) Public pages: short cache app.get("/", (req, res) => { res.setHeader("Cache-Control", "public, max-age=300"); // 5 min res.send("<h1>Hello</h1>"); }); // 3) Authenticated pages: do not cache app.get("/dashboard", (req, res) => { // if this depends on cookies/session, prevent caching res.setHeader("Cache-Control", "private, no-store"); res.send("<h1>Dashboard</h1>"); }); app.listen(3000);
Tip: no-store is stronger than no-cache. Use no-store for anything sensitive or user-specific.
3.2 Example: Nginx headers for static + HTML
server { listen 80; server_name example.com; root /var/www/site; # Fingerprinted assets location /assets/ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # Public HTML (short cache) location = / { add_header Cache-Control "public, max-age=300"; try_files /index.html =404; } # Auth pages (never cache) location /dashboard/ { add_header Cache-Control "private, no-store"; proxy_pass http://app:3000; } }
4) Use Cloudflare Cache Rules (safe, targeted)
Cloudflare’s Cache Rules let you define caching behavior based on URL patterns, methods, headers, etc. A practical rule set looks like this:
- Rule A: Cache static assets
- If URL path starts with
/assets/or matches*.css,*.js,*.png,*.jpg,*.svg - Cache eligibility: “Eligible for cache”
- Edge TTL: 1 year (or “Respect origin” if your origin headers are correct)
- If URL path starts with
- Rule B: Bypass authenticated routes
- If URL path starts with
/dashboardor/accountor/api - Cache eligibility: “Bypass cache”
- If URL path starts with
- Rule C: Cache marketing pages carefully
- If path matches
/,/pricing,/docs - Edge TTL: 5 minutes (or “Respect origin” if you set
max-age=300)
- If path matches
Why both headers and rules? Headers are portable and work across CDNs. Rules give you “seatbelts” and quick overrides without redeploying.
5) Handle “cache HTML unless logged in” with a Worker
Sometimes you want to cache the home page for anonymous visitors, but bypass cache if a session cookie exists. Cloudflare Workers can do this safely by changing cache behavior based on cookies.
Below is a minimal Worker that:
- Caches
GETrequests for public pages - Bypasses cache if it detects a login cookie (
session) - Sets a short edge cache TTL for HTML
export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Only cache GET/HEAD if (request.method !== "GET" && request.method !== "HEAD") { return fetch(request); } // Skip caching for sensitive paths if (url.pathname.startsWith("/dashboard") || url.pathname.startsWith("/api")) { return fetch(request); } const cookie = request.headers.get("Cookie") || ""; const isLoggedIn = cookie.includes("session="); // If logged in, bypass cache if (isLoggedIn) { const resp = await fetch(request, { cf: { cacheTtl: 0, cacheEverything: false } }); // Ensure downstream caches don't store it const newResp = new Response(resp.body, resp); newResp.headers.set("Cache-Control", "private, no-store"); return newResp; } // Anonymous: cache HTML briefly at the edge const resp = await fetch(request, { cf: { cacheEverything: true, cacheTtl: 300, // 5 minutes at Cloudflare edge }, }); // Optional: make intent visible const newResp = new Response(resp.body, resp); // Keep browser caching conservative; edge does the heavy lifting newResp.headers.set("Cache-Control", "public, max-age=0, s-maxage=300"); return newResp; }, };
Notes:
s-maxageis respected by shared caches; browsers usemax-age.- If your HTML changes frequently, keep TTL short (1–5 minutes) and rely on purge for urgent updates.
6) Purge cache the right way (deploy-friendly)
Even with perfect headers, you’ll eventually need to purge. Two practical patterns:
- Best: Use content-hashed filenames for assets (e.g.,
app.9f3a1c2b.js). Deploying new assets automatically “busts” cache because URLs change. - For HTML: Purge specific URLs after deploy (home page, key marketing pages). Avoid “purge everything” unless you’re troubleshooting.
If you have a deployment script, you can call Cloudflare’s API to purge selected URLs. Example (Node 18+):
async function purgeUrls({ zoneId, apiToken, urls }) { const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`, { method: "POST", headers: { "Authorization": `Bearer ${apiToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ files: urls }), }); const data = await res.json(); if (!data.success) { throw new Error(`Purge failed: ${JSON.stringify(data.errors)}`); } return data; } // Usage purgeUrls({ zoneId: process.env.CF_ZONE_ID, apiToken: process.env.CF_API_TOKEN, urls: [ "https://example.com/", "https://example.com/pricing", ], }).then(() => console.log("Purged"));
7) Quick troubleshooting checklist
- Asset isn’t caching: Check
Cache-Controlfrom origin, and confirm Cloudflare sees it as cacheable (inspectcf-cache-status). - HTML caching when it shouldn’t: Ensure
Cache-Control: private, no-storeon authenticated responses, and add a “Bypass” Cache Rule for auth paths. - Cache fragmentation: Query strings can create many variants. If you use marketing params (
utm_*), consider a rule/Worker that strips them for caching. - Unexpected BYPASS: Cloudflare may bypass cache for responses with
Set-Cookie. Don’t set cookies on pages you want cached for anonymous users.
8) A sane “starter configuration” you can copy
If you want a safe baseline for most web apps:
- Origin
/assets/*→Cache-Control: public, max-age=31536000, immutable- Public HTML →
Cache-Control: public, max-age=300 - Authenticated/API →
Cache-Control: private, no-store
- Cloudflare Cache Rules
- Cache assets aggressively
- Bypass cache for
/api,/dashboard,/account - Optional: cache public HTML with short edge TTL
- Optional Worker
- Cache HTML only for anonymous users
- Force
no-storewhen session cookie exists
With this setup, you’ll get faster global load times, fewer origin hits, and far less risk of caching something sensitive. Start simple, measure with curl -I and browser devtools, then tighten rules as you learn your traffic patterns.
Leave a Reply