# CDN with Cloudflare: How to Properly Serve Static Assets at the Edge

> A practical guide to configuring Cloudflare as a CDN for static assets — cache rules, TTLs, cache hit optimization, and tiered caching that actually cuts origin load.

URL: https://www.matthewswong.com/en/blog/cdn-cloudflare-static/

- Author: Matthews Wong (https://www.matthewswong.com/en/about/)
- Published: 2025-06-10
- Updated: 2026-10-17
- Category: DevOps

I put web applications behind Cloudflare by default. Not just for DDoS protection and the free SSL certificate — primarily for the CDN layer that reduces latency for users in Indonesia and eliminates a significant fraction of origin server load. But Cloudflare's default caching behavior is conservative: it only caches static file extensions (CSS, JS, images) and ignores HTML and API responses. Getting Cloudflare to cache most of your traffic requires deliberate configuration with Cache Rules. This guide covers how I configure Cloudflare for static-heavy Next.js and React applications.

## Understanding Cloudflare's Default Cache Behavior

By default, Cloudflare caches a specific list of file extensions: images (jpg, jpeg, png, gif, webp, svg, ico), stylesheets (css), scripts (js), fonts (woff, woff2, ttf), and a few others. Anything not on this list — HTML, JSON, XML, API responses — passes through to your origin on every request. The default browser cache TTL for cached assets is 4 hours. The default edge cache TTL honors your origin's Cache-Control header. If your origin doesn't set Cache-Control: max-age=xxx, Cloudflare uses its own default (varies by plan). The free plan's default edge TTL for content without Cache-Control headers is 4 hours.

### Cache Rules: The Right Way to Configure Caching

Cache Rules (formerly Page Rules — Cloudflare migrated all Page Rules to Cache Rules in 2024) let you specify which resources get cached, for how long, and with what behavior. A Cache Rule for static assets matches requests to /static/*, /_next/static/*, or /images/* and sets Edge Cache TTL to 1 year, Browser Cache TTL to 1 month, and Cache Level to Standard. These long TTLs work because Next.js content-hashes its static asset filenames — when a JS bundle changes, the filename changes, busting the cache automatically. The rule is safe to set aggressively.

### Cache Hit Rate Optimization

A common issue I see is cache hit rates stuck at 30-40% even with rules configured. The causes: query strings creating unique cache keys for the same asset, cookies preventing caching, and vary headers forcing separate cache entries per Accept-Encoding value. To diagnose, check the CF-Cache-Status response header — MISS means the resource wasn't cached, HIT means it was served from cache, BYPASS means a rule or setting prevented caching. Use Cloudflare's Cache Analytics (available on free plan) to see hit rates by URL pattern and identify which resources are contributing most to cache misses.

> From my experience: the single highest-impact Cloudflare configuration for a Next.js app is enabling Tiered Cache under Caching → Tiered Cache → Smart Tiered Cache Topology. Tiered Cache creates a hierarchy where edge PoPs (points of presence) check an upper-tier PoP before going to your origin. For popular assets, one upper-tier PoP fetches from your origin, and all lower-tier PoPs in the region fetch from the upper-tier PoP. For a site with users in Jakarta and Surabaya (served by different Cloudflare PoPs), Tiered Cache ensures the Surabaya PoP fetches from Cloudflare's Singapore hub rather than hitting your Singapore-region origin repeatedly.

## Configuring Cache Rules for a Next.js Application

A Next.js application has predictable asset patterns. Static assets under /_next/static/ are content-addressed and safe to cache for 1 year. The Next.js Image Optimization API at /_next/image generates optimized images on first request and caches them — configure Cloudflare to cache these with a 24-hour edge TTL to avoid redundant optimization runs. HTML pages require careful decisions: Next.js ISR pages have their own revalidation logic, and aggressive Cloudflare HTML caching can interfere. I recommend caching HTML only for truly static pages (blog posts, landing pages) and bypassing cache for authenticated or personalized pages.

### Custom Cache Keys and Query String Handling

Query strings break caching when the same content is requested with different query parameters. A JavaScript file requested as bundle.js?v=1 and bundle.js?v=2 creates two separate cache entries. Configure Cache Rules to ignore query strings for static assets using the 'Cache Key' setting — set Query String to 'Ignore'. For pages where query strings change content (search results, filtered lists), keep query strings in the cache key or bypass cache entirely. For API endpoints, bypass cache entirely unless the response is explicitly public and identical for all users with the same parameters.

> **Don't cache API responses without understanding your headers**

> I once configured a Cloudflare Cache Rule that matched /api/* with a 5-minute edge cache TTL to reduce load on a Node.js API. The rule worked — cache hit rate jumped from 0% to 60%. But I hadn't accounted for user-specific responses: the user profile API at /api/user/profile was now returning cached responses from other users' requests. Cloudflare was caching responses that contained private data because the origin wasn't setting Cache-Control: private. Always set Cache-Control: private, no-store on any response that contains user-specific data. A Cloudflare Cache Rule should never override private responses.

## Cloudflare R2 vs Cloud Storage Origin for Static Assets

For truly static assets (images, fonts, downloadable files), hosting on Cloudflare R2 rather than GCP Cloud Storage or DigitalOcean Spaces eliminates egress costs entirely — R2 has no egress fees. Assets served from R2 through Cloudflare's network are already at the edge, so they serve from Cloudflare's cache without a round trip to an origin in a specific region. For media files that don't change frequently, R2 + Cloudflare CDN is both faster and cheaper than GCS + Cloudflare CDN. I migrated our client's product image catalog (80GB) from GCS to R2 and eliminated $6.80/month in GCS egress costs.

## Monitoring Cache Performance

Cloudflare provides built-in analytics showing cache hit rates, bandwidth saved, and request counts by cache status. Set a goal of 70%+ cache hit rate for a static-heavy site. Below 60% usually indicates a configuration issue worth investigating. The CF-Cache-Status header in responses tells you exactly what happened for each request: HIT (served from cache), MISS (not in cache, fetched from origin), BYPASS (caching skipped due to a rule or cookie), EXPIRED (in cache but TTL expired, re-fetched from origin), and DYNAMIC (Cloudflare determined the content is dynamic and skipped caching).

## My Take: Cloudflare Is Worth It for Every Project

I put every project behind Cloudflare from day one, even personal side projects. The free plan includes unlimited CDN bandwidth, free SSL via Let's Encrypt, DDoS protection, and basic cache rules. For projects that need more, a paid plan adds image optimization, more Cache Rule capacity, and Web Application Firewall. The return shows up quickly: reduced origin server load means smaller VM sizes, lower cloud egress bills, and faster response times for Indonesian users who are geographically close to Cloudflare's Singapore and Jakarta edge nodes.

## Sources & Further Reading

- Cloudflare — Cache Rules Documentation — https://developers.cloudflare.com/cache/how-to/cache-rules/
- Cloudflare — Default Cache Behavior — https://developers.cloudflare.com/cache/concepts/default-cache-behavior/
- DebugBear — How to Cache Your Website on Cloudflare — https://www.debugbear.com/docs/cloudflare-caching

## Source links

- https://developers.cloudflare.com/cache/how-to/cache-rules/
- https://developers.cloudflare.com/cache/concepts/default-cache-behavior/
- https://www.debugbear.com/docs/cloudflare-caching

## FAQ

### Why doesn't Cloudflare cache everything by default, and what do you need to do to fix it?

Cloudflare's default caching is conservative — it only caches a specific list of static file extensions like CSS, JS, and images, while HTML, JSON, and API responses pass through to your origin on every request. To cache 70–90% of your traffic you need to configure Cache Rules (formerly Page Rules, migrated in 2024) that explicitly define which URL patterns to cache, for how long, and with what TTL settings.

### Why is it safe to set a 1-year TTL for Next.js static assets on Cloudflare?

Next.js content-hashes its static asset filenames — when a JS bundle changes, the filename itself changes, which automatically busts the Cloudflare cache. Because the URL is always unique to the file's content, stale cache entries are never served to users. This makes it safe to set Edge Cache TTL to 1 year and Browser Cache TTL to 1 month for assets under /_next/static/.

### What is Tiered Cache and why is it the highest-impact Cloudflare setting for a Next.js app?

Tiered Cache creates a hierarchy where regional edge PoPs check an upper-tier PoP before going to your origin server. For a site with users in Jakarta and Surabaya, the Surabaya PoP fetches from Cloudflare's Singapore hub rather than hitting your origin repeatedly. This dramatically reduces origin load for popular assets and is enabled under Caching → Tiered Cache → Smart Tiered Cache Topology.

### What went wrong when caching /api/* routes, and how should API caching be handled?

Caching /api/* with a 5-minute edge TTL caused the user profile endpoint at /api/user/profile to return other users' cached private data, because the origin was not setting Cache-Control: private on those responses. The cache hit rate rose from 0% to 60%, but private data leaked across users. The fix is to always set Cache-Control: private, no-store on any user-specific response, and to bypass Cloudflare cache for API endpoints unless responses are explicitly public and identical for all users.

### When does migrating static assets from GCP Cloud Storage to Cloudflare R2 make financial sense?

R2 has no egress fees, so assets served through Cloudflare's network are delivered from the edge without a round trip to a regional origin. For media files that change infrequently, R2 + Cloudflare CDN is both faster and cheaper than GCS + Cloudflare CDN. Migrating an 80 GB product image catalog from GCS to R2 eliminated $6.80/month in GCS egress costs.

---

- HTML version: https://www.matthewswong.com/en/blog/cdn-cloudflare-static/
- Bahasa Indonesia: https://www.matthewswong.com/id/blog/cdn-cloudflare-static/
- Site index for agents: https://www.matthewswong.com/llms.txt
- Sitemap: https://www.matthewswong.com/sitemap.xml
- API description (OpenAPI): https://www.matthewswong.com/openapi.json
