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 onCookie/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
Cookieheaders 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 withAuthorizationor 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-Languageif 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 usecurl -Ito 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 -Iand watchCF-Cache-Status,Age, andCache-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