Cloudflare CDN in Practice: Cache Your Site (and APIs) Without Breaking Things

Cloudflare CDN in Practice: Cache Your Site (and APIs) Without Breaking Things

A CDN sounds simple: “put Cloudflare in front of my site and it gets faster.” In reality, performance gains come from making caching predictable. This guide walks through a practical setup you can apply to most web apps (static + dynamic), with hands-on headers, rules, a Worker example for API caching, and safe ways to purge and verify.

1) The mental model: what Cloudflare will (and won’t) cache by default

Cloudflare caches automatically when your origin serves “static” assets (e.g., .css, .js, images) and the response is cacheable. For HTML pages and API responses, Cloudflare usually behaves conservatively unless you tell it otherwise.

  • Static assets: cacheable by default if headers allow.
  • HTML: typically not cached unless you use Cache Rules / Page Rules (legacy) or explicit headers + config.
  • APIs: generally not cached unless you intentionally cache them (and you should be careful).

Your job is to:

  • Set clear cache headers at the origin.
  • Create Cloudflare rules to respect those headers (or override where safe).
  • Exclude anything user-specific (cookies, auth, personalized HTML) from caching.
  • Verify with repeatable tests.

2) Start with origin headers: “cache-control first”

Even if you plan to use Cloudflare rules, good origin headers make everything easier (and portable). Here’s a solid baseline:

  • Fingerprint static assets (e.g., app.8c1d2a3.js) and cache them “forever”.
  • HTML usually short-lived or not cached unless you’re doing full-page caching intentionally.
  • APIs cache only if responses are public and identical for all users (or you control the cache key).

Example: Nginx headers for static assets

server { # ... your usual config ...
location /assets/ {
# For hashed filenames: app.abcd1234.js, styles.ef567890.css
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
location / {
# Safer default for HTML if you have sessions/personalization
add_header Cache-Control "no-store";
try_files $uri /index.html;
}
}

Why immutable? It tells browsers/CDNs the file will never change, so they don’t revalidate. This is safe only if filenames change when content changes (fingerprinting).

3) Add Cloudflare Cache Rules (practical presets)

In Cloudflare’s dashboard, you can create Cache Rules to define when to cache, TTL, and what to bypass. A good “starter set”:

  • Rule A (Cache static assets aggressively): Match http.request.uri.path starts with /assets/ (or your static directory). Set “Cache status: Eligible” and respect origin headers (or override with a long Edge TTL).
  • Rule B (Bypass admin/auth paths): Match /admin, /account, /checkout, /api/auth. Set “Bypass cache”.
  • Rule C (Optional full-page caching for marketing pages): Match routes like /, /pricing, /docs if they are not personalized. Set Edge TTL (e.g., 5–30 minutes) and ensure cookies don’t make it user-specific.

Junior-friendly rule of thumb: if a page changes based on who is logged in, don’t cache it as HTML at the edge.

4) Verify caching with curl (don’t guess)

Cloudflare adds useful response headers. The two you’ll check most:

  • CF-Cache-Status: HIT, MISS, BYPASS, EXPIRED, etc.
  • Age: how long (in seconds) the object has been in cache (when applicable).
# First request (likely MISS) curl -I https://example.com/assets/app.8c1d2a3.js # Second request (should become HIT) curl -I https://example.com/assets/app.8c1d2a3.js

If you see BYPASS unexpectedly, common reasons include:

  • Response has Cache-Control: no-store or private
  • Set-Cookie is being sent on a resource that should be cacheable
  • A Cloudflare rule is bypassing cache

5) Safe API caching with a Cloudflare Worker (hands-on)

Caching APIs is where teams break stuff. But it can be extremely effective for “public” endpoints like:

  • Product catalogs
  • Public blog lists
  • Read-only configuration
  • Search suggestions (careful with query keys)

Below is a minimal Worker that caches GET requests to /api/public/* for 60 seconds at the edge. It also controls the cache key so query strings are included (good for endpoints like ?page=2).

// Cloudflare Worker (module syntax) export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Only cache GET requests to a safe public namespace const isCacheableApi = request.method === "GET" && url.pathname.startsWith("/api/public/"); if (!isCacheableApi) { return fetch(request); } // Build an explicit cache key (include query string) const cacheKey = new Request(url.toString(), { method: "GET", headers: { // Strip cookies and auth headers from cache key usage // so a logged-in user doesn't poison the cache. }, }); const cache = caches.default; let response = await cache.match(cacheKey); if (response) { return response; } // Fetch from origin response = await fetch(request, { headers: { ...Object.fromEntries(request.headers), // Ensure we don't forward cookies if you don't need them for public endpoints "Cookie": "", "Authorization": "", }, }); // Only cache successful responses if (response.ok) { // Clone because response bodies are streams const resToCache = new Response(response.body, response); // Add explicit edge caching headers resToCache.headers.set("Cache-Control", "public, max-age=60"); // Cache at the edge ctx.waitUntil(cache.put(cacheKey, resToCache.clone())); return resToCache; } return response; } };

Notes:

  • We intentionally avoid caching anything that depends on user identity.
  • We only cache response.ok (2xx). You can expand that, but be deliberate.
  • We strip Cookie and Authorization for “public” endpoints. If your origin requires auth for these endpoints, don’t cache them this way.

6) Purge cache surgically (and automate it)

When you deploy a new version, you typically don’t need to purge fingerprinted assets (their filenames changed). You may need to purge:

  • HTML pages you edge-cache (marketing pages, docs)
  • API responses cached at the edge

Example: Purge a single URL using Cloudflare’s API

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://example.com/pricing","https://example.com/api/public/catalog?page=1"]}'

Tip: Prefer purging specific URLs over “purge everything,” especially on high-traffic sites.

7) Common pitfalls and how to avoid them

  • Accidentally caching personalized HTML: If you cache pages that vary by cookies or auth, users may see other users’ data. Default HTML to no-store unless you’re sure.
  • Setting cookies on static assets: A stray Set-Cookie on .js can kill caching. Make sure your app/framework doesn’t attach session cookies globally.
  • Query string chaos: If your URLs have many tracking params (utm_*), they can explode your cache. Normalize URLs (remove tracking params) or adjust cache key behavior in rules/Workers.
  • Stale content after deploy: Use short Edge TTL for HTML, or purge on deploy. For APIs, keep TTL short unless you have a robust invalidation strategy.

8) A simple rollout plan you can follow

  • Step 1: Ensure fingerprinted assets + long-lived cache headers.
  • Step 2: Add Cloudflare Cache Rule for static assets.
  • Step 3: Add bypass rules for auth/admin/user pages.
  • Step 4: Verify with curl -I and confirm CF-Cache-Status: HIT for static assets.
  • Step 5: Optionally edge-cache a few safe HTML routes with short TTL.
  • Step 6: Add Worker-based caching for one public API endpoint; measure before expanding.

If you implement just the static-asset part correctly, you’ll often see a noticeable real-world speedup with minimal risk. Then you can layer on edge-cached HTML and selective API caching once you’ve built confidence and observability.


Leave a Reply

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