Cloudflare CDN in Practice: Cache Static + Dynamic Content Safely (with Cache Rules & Workers)

Cloudflare CDN in Practice: Cache Static + Dynamic Content Safely (with Cache Rules & Workers)

Cloudflare can make a slow site feel fast—but only if you cache the right things, avoid caching the wrong things, and can debug what’s happening at the edge. This hands-on guide shows a practical setup for junior/mid developers: how to cache static assets aggressively, cache selected dynamic responses safely, add an edge “micro-cache” for expensive endpoints, and verify everything with real commands.

1) Know what you should (and shouldn’t) cache

A good default rule: cache things that are identical for everyone, and avoid caching anything user-specific.

  • Great candidates: .css, .js, images, fonts, public downloads, versioned build assets (app.8c3f1.js).
  • Often cacheable with care: marketing pages, blog pages, product pages (if same for all users), API GET endpoints that don’t depend on user auth.
  • Never cache publicly: HTML that changes per user, pages behind login, responses that include tokens, cart/checkout, /me, or any endpoint depending on Cookie/Authorization.

Cloudflare will respect your origin headers (Cache-Control, ETag, Last-Modified) unless you override them with Cache Rules / Workers. Start simple: make static assets cacheable forever, and then selectively add caching for dynamic routes.

2) Step 1: Cache static assets “forever” (with safe versioning)

The key is cache-busting filenames (hashes) and long-lived cache headers. If filenames include a content hash, you can tell browsers and Cloudflare to cache for a year.

Nginx example (works similarly in other servers):

server { listen 80; server_name example.com; root /var/www/app/public; # Static assets: cache for 1 year; immutable is safe with hashed filenames location ~* \.(css|js|mjs|png|jpg|jpeg|gif|svg|webp|ico|woff2?)$ { add_header Cache-Control "public, max-age=31536000, immutable"; try_files $uri =404; } # HTML: keep conservative by default location / { add_header Cache-Control "no-cache"; try_files $uri /index.html; } } 

If you don’t have hashed filenames yet (for example, you serve app.js), don’t use immutable with a year TTL. Use something shorter like max-age=3600 until you can version your assets.

3) Step 2: Configure Cloudflare Cache Rules for static routes

In Cloudflare, you can create a Cache Rule to “Cache Everything” for specific paths—useful when your origin headers aren’t ideal or you want a standard policy.

  • Rule match: (http.request.uri.path matches ".*\\.(css|js|png|jpg|svg|woff2)$")
  • Action: Cache → Eligible for cache (or “Cache Everything” if needed)
  • Edge TTL: e.g. 30 days (Cloudflare edge)
  • Browser TTL: e.g. Respect origin or set a long TTL for versioned files

Prefer “respect origin headers” when possible, and only override when your app needs it.

4) Step 3: Verify caching behavior with curl (no guessing)

Once a request goes through Cloudflare, you should see headers like CF-Cache-Status.

# First request (likely MISS) curl -I https://example.com/assets/app.8c3f1.js # Second request (should become HIT if cacheable) curl -I https://example.com/assets/app.8c3f1.js 

Look for:

  • CF-Cache-Status: HIT (served from edge cache)
  • cache-control (your policy)
  • age (seconds the asset has been in cache)

If you keep seeing MISS, check that the response is cacheable (Cache-Control doesn’t include private or no-store), and that Cloudflare isn’t bypassing cache due to cookies or rule conditions.

5) Step 4: Add an edge “micro-cache” for expensive GET endpoints

Sometimes the biggest win isn’t static assets—it’s caching a heavy endpoint for 5–30 seconds to absorb traffic spikes. This is called micro-caching. It’s especially useful for:

  • Unauthenticated pages like / or /blog
  • Public API endpoints like GET /api/products
  • Search endpoints with rate limits or expensive DB queries

For micro-caching, Cloudflare Workers give you fine control: you can cache based on URL + query string, and bypass cache if a session cookie is present.

6) Worker example: Cache public JSON for 15 seconds, bypass for logged-in users

Create a Cloudflare Worker and route it to your API path (e.g. example.com/api/*). The Worker below caches GET /api/products for 15 seconds, but skips caching if it detects an auth header or a session cookie.

export default { async fetch(request, env, ctx) { const url = new URL(request.url); // Only micro-cache one endpoint in this example const isTarget = request.method === "GET" && url.pathname === "/api/products"; if (!isTarget) return fetch(request); // Bypass cache for authenticated requests (customize for your app) const auth = request.headers.get("Authorization"); const cookie = request.headers.get("Cookie") || ""; const hasSession = cookie.includes("session=") || cookie.includes("jwt="); if (auth || hasSession) { return fetch(request, { cf: { cacheTtl: 0, cacheEverything: false } }); } // Cache key: include query string to avoid mixing filters const cacheKey = new Request(url.toString(), request); // Tell Cloudflare to cache this response at the edge for 15 seconds const response = await fetch(cacheKey, { cf: { cacheTtl: 15, cacheEverything: true, }, }); // Add a helpful debug header (visible in curl) const newHeaders = new Headers(response.headers); newHeaders.set("Cache-Control", "public, max-age=15"); // browser hint; edge TTL is above return new Response(response.body, { status: response.status, headers: newHeaders, }); }, }; 

Test it:

# First request (MISS) curl -i https://example.com/api/products | grep -iE "cf-cache-status|cache-control" # Second request (HIT within 15s) curl -i https://example.com/api/products | grep -iE "cf-cache-status|cache-control" 

If you see CF-Cache-Status: HIT, your micro-cache is working. If it’s always MISS, confirm the Worker route is correct and that the endpoint isn’t sending Cache-Control: no-store from the origin (your Worker can override, but you should be intentional).

7) Purging: when to purge, and when to avoid it

Purging cache is sometimes necessary, but frequent global purges can reduce cache hit rates and cause traffic spikes to your origin.

  • Prefer: versioned filenames for assets (no purge needed).
  • Purge only what changed: a URL or a small set of URLs.
  • Use micro-cache TTLs: for dynamic endpoints so stale data disappears quickly without purges.

Practical workflow:

  • Static assets: deploy new hashed assets → update HTML references → no purge.
  • Blog post edited: purge only that post URL (and maybe its listing page).
  • API data refresh: rely on a 15–60s micro-cache TTL rather than purging every time.

8) Common pitfalls (and quick fixes)

  • Everything is a MISS: Check for cookies. Some setups bypass cache when Cookie headers are present. Fix by serving static assets from a cookie-less domain (e.g. static.example.com) or ensure your cache rules don’t vary on cookies.

  • Users see cached logged-in pages: Don’t “Cache Everything” on routes that can be personalized. Add explicit bypass conditions for /account, /checkout, and any request with Authorization or session cookies.

  • API responses mix query variants: Make sure your cache key includes the query string (Workers do by default when you use the full URL). Be careful with headers like Accept-Language if you serve localized content; you may need to vary the cache key.

  • Hard-to-debug behavior: Add small debug headers in Workers (like X-Edge-Cache: micro) and use curl -I to confirm what happened.

9) A sensible “starter blueprint”

  • Static: hashed filenames + Cache-Control: public, max-age=31536000, immutable

  • HTML: conservative caching (no-cache), then selectively micro-cache public pages if needed

  • APIs: micro-cache only safe public GET endpoints (5–30s), bypass on auth/cookies

  • Verification: use curl -I and watch CF-Cache-Status, Age, and Cache-Control

Wrap-up

The easiest way to get real performance from Cloudflare is to treat caching as a product feature: start with “static forever,” then add micro-caching to the few endpoints that are expensive and safe to cache. Use Cache Rules for broad strokes and Workers when you need logic (like bypassing logged-in sessions). And always verify with headers—CDNs feel magical until you can’t explain a MISS.


Leave a Reply

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