Cloudflare CDN in Practice: Cache Static Assets, Bypass Dynamic Pages, and Add Edge Caching with a Worker

Cloudflare CDN in Practice: Cache Static Assets, Bypass Dynamic Pages, and Add Edge Caching with a Worker

Cloudflare can make your site feel “instantly faster” for users by serving cached content from edge locations near them. But the real wins come when you cache the right things, avoid caching the wrong things, and automate cache invalidation for deployments.

This hands-on guide walks through a pragmatic setup that works well for many web apps:

  • Cache static assets aggressively (hashed filenames).
  • Bypass cache for dynamic/authenticated routes.
  • Add a Cloudflare Worker for edge-cached API reads (optional, but powerful).
  • Purge cache by URL (or everything) during deploys.

All examples are “copy/paste and adapt” friendly for junior/mid developers.

1) Understand Cloudflare’s default caching behavior (so you don’t fight it)

By default, Cloudflare caches “static-like” files (images, CSS, JS, fonts) and generally won’t cache HTML unless you explicitly tell it to (via Cache Rules / Workers). Cloudflare’s modern approach is to use Cache Rules to define what is eligible for caching and for how long. :contentReference[oaicite:0]{index=0}

Goal: treat assets and API GET responses differently from personalized HTML pages.

2) Cache static assets the “right way”: long TTL + filename hashing

If your frontend build produces hashed asset filenames (e.g. app.9f3a12c.js), you can safely cache them “forever” because any change creates a new filename.

At your origin (Nginx example), add long-lived caching headers for hashed assets:

# /etc/nginx/sites-enabled/your-site.conf location /assets/ { # Assumes files are fingerprinted: app.[hash].js, styles.[hash].css, etc. add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # Optional: fonts location ~* \.(woff2|woff|ttf|otf)$ { add_header Cache-Control "public, max-age=31536000, immutable"; }

Why this matters:

  • max-age=31536000 is one year.
  • immutable tells browsers not to revalidate if the URL hasn’t changed.
  • Cloudflare will also respect these headers in many configurations, improving edge hit rate.

If you don’t have hashed filenames yet, do that first. It’s the single simplest “no-regrets” performance upgrade.

3) Use Cache Rules to cache static content and bypass dynamic routes

In Cloudflare’s dashboard, Cache Rules let you match requests (by hostname, path, file extension, headers, etc.) and set caching behavior. :contentReference[oaicite:1]{index=1}

A practical baseline:

  • Rule A (Cache static assets): match /assets/*, set Edge TTL to something long (e.g. 30 days), respect origin headers if you prefer.
  • Rule B (Bypass cache for dynamic): match /api/* (unless you explicitly cache API), /dashboard/*, /account/*, and anything that is user-specific.
  • Rule C (Bypass when authenticated): if you use cookies like session or jwt, avoid caching HTML responses that vary per user.

Tip: Don’t “Cache Everything” on your whole site unless you truly understand your app’s caching story—otherwise you’ll eventually cache personalized pages and ship the wrong content to the wrong user. If you do need HTML caching, do it with narrow rules (like only the marketing pages) or a Worker.

4) Add edge caching for GET API responses with a Cloudflare Worker

Sometimes you want a fast API without hammering your origin—especially for public, cacheable reads like:

  • GET /api/products
  • GET /api/blog
  • GET /api/config (public settings)

Cloudflare Workers can cache responses at the edge using the Cache API. :contentReference[oaicite:2]{index=2}

Here’s a Worker that:

  • Only caches GET
  • Skips caching when an Authorization header is present
  • Caches successful responses for 60 seconds (adjustable)
  • Returns an X-Edge-Cache header so you can see HIT/MISS
// worker.js export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Only apply to a specific API prefix if (!url.pathname.startsWith("/api/")) { return fetch(request); } // Cache only GET, and only when unauthenticated if (request.method !== "GET" || request.headers.has("Authorization")) { return fetch(request); } const cache = caches.default; // Normalize the cache key (example: ignore tracking params) url.searchParams.delete("utm_source"); url.searchParams.delete("utm_medium"); url.searchParams.delete("utm_campaign"); const cacheKey = new Request(url.toString(), request); // Try cache first let response = await cache.match(cacheKey); if (response) { response = new Response(response.body, response); response.headers.set("X-Edge-Cache", "HIT"); return response; } // If not cached, fetch from origin and cache it (only if OK) response = await fetch(request, { cf: { // Tells Cloudflare to cache even if origin headers are not cache-friendly cacheEverything: true, cacheTtl: 60 } }); if (response.ok) { // Store a clone in cache, return the original ctx.waitUntil(cache.put(cacheKey, response.clone())); } const out = new Response(response.body, response); out.headers.set("X-Edge-Cache", "MISS"); return out; } };

Notes:

  • cf.cacheEverything and cf.cacheTtl let Workers influence caching behavior for a fetch. :contentReference[oaicite:3]{index=3}
  • The Worker approach is great for read-heavy endpoints that don’t need perfect real-time freshness.
  • Start with a short TTL (30–120 seconds). You can always increase it later.

5) Cache busting and deploys: purge by URL (or purge everything)

Even with good caching, you’ll eventually need to invalidate cached content during deploys—especially if you cache HTML or API responses at the edge.

Cloudflare supports “Instant Purge” and recommends purging specific URLs when possible. :contentReference[oaicite:4]{index=4}

Purge specific files by URL (recommended):

# Requires: CLOUDFLARE_API_TOKEN with cache purge permission # and your ZONE_ID curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{ "files": [ "https://example.com/", "https://example.com/api/products" ] }'

Purge everything (use sparingly):

curl -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \ -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ -H "Content-Type: application/json" \ --data '{"purge_everything": true}'

Both calls hit the same endpoint; only the JSON body changes. :contentReference[oaicite:5]{index=5}

Practical deploy pattern: If your assets are hashed, you rarely need to purge them. You mostly purge:

  • Home page HTML (/) if you cache it
  • Marketing pages that changed
  • Edge-cached API endpoints that changed

6) Quick debugging checklist (so you can trust what’s happening)

When caching “doesn’t work,” it’s usually one of these:

  • Wrong cache key: query params or headers create multiple cache entries (use Cache Rules cache keys or normalize in a Worker). :contentReference[oaicite:6]{index=6}
  • Cookies/Authorization present: Cloudflare may bypass cache, or you should bypass to avoid leaking personalized data.
  • Origin sends Cache-Control: private or no-store: either fix origin headers or override caching in a Worker for safe endpoints.
  • Expecting global replication from Worker Cache API: the Cache API is “per data center” unless you design around it (TTL + natural traffic works well). :contentReference[oaicite:7]{index=7}

Helpful habit: add an X-Cache-Debug or X-Edge-Cache header (as shown in the Worker) so you can verify HIT/MISS from your browser devtools or curl.

7) A sensible “starter config” you can apply today

  • Origin: long-lived caching headers for hashed assets (immutable).
  • Cloudflare Cache Rules:
    • Cache /assets/* with long Edge TTL.
    • Bypass cache for authenticated/dynamic routes.
  • Optional Worker: edge-cache public GET /api/* reads with 60s TTL.
  • Deploy automation: purge only URLs that matter (HTML pages and a few API endpoints).

If you implement just those pieces, you’ll usually see faster repeat visits, fewer origin hits, and a more stable experience under load—without risking “cached personalized pages” disasters.

::contentReference[oaicite:8]{index=8}


Leave a Reply

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