Cloudflare CDN in Practice: Cache What Matters, Bypass What Doesn’t (Hands-On)

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)
  • Rule B: Bypass authenticated routes
    • If URL path starts with /dashboard or /account or /api
    • Cache eligibility: “Bypass cache”
  • Rule C: Cache marketing pages carefully
    • If path matches /, /pricing, /docs
    • Edge TTL: 5 minutes (or “Respect origin” if you set max-age=300)

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 GET requests 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-maxage is respected by shared caches; browsers use max-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-Control from origin, and confirm Cloudflare sees it as cacheable (inspect cf-cache-status).
  • HTML caching when it shouldn’t: Ensure Cache-Control: private, no-store on 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-store when 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

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