Skip to content

Cloudflare Doesn't Cache Your HTML Unless You Ask

A 7%-to-90% cache hit rate case study, plus the staleness tax the write-up skips.

Emeka Okafor
Emeka Okafor
Security Editor · Aug 2, 2026 · 5 min read
Cloudflare Doesn't Cache Your HTML Unless You Ask

A developer shipped a static Next.js site to Cloudflare Pages and found users in China staring at a blank screen for 20 seconds. The cache hit rate on a fully static site — no server, no database, nothing dynamic anywhere — was 7.62%. Three Cache Rules later it was above 90%, and time-to-first-byte on cache hits dropped from multiple seconds to a few hundred milliseconds.

Those numbers come from one self-reported case study of utlkit.com, so treat the exact figures as anecdote. But the mechanism behind them is documented, reproducible, and widely misunderstood: Cloudflare does not cache your HTML unless you explicitly tell it to. If you've never checked, there's a decent chance your "edge-cached" site is round-tripping to origin on every page view.

The default is "don't cache the thing that matters"

Cloudflare's CDN decides cacheability by file extension, not content type. The default list covers .js, .css, images, fonts — and pointedly excludes HTML and JSON. Request a page, and the response comes back cf-cache-status: DYNAMIC, which is Cloudflare's way of saying "we didn't even try."

This is a defensible default. Cloudflare fronts millions of zones and has no idea whether your HTML is a static marketing page or a logged-in dashboard; caching it blind would break sessions everywhere. But it collides badly with how developers think about static hosting platforms. Pages serves every asset with Cache-Control: public, max-age=0, must-revalidate — browsers revalidate on every visit — and its _headers file only shapes the response headers your visitors see. It does not instruct the edge to cache anything. That's the trap in this case study: the author spent time tuning _headers believing it controlled edge caching. It doesn't. Edge behavior lives in Cache Rules, a completely separate system.

So the "static site on a global CDN" mental model quietly degrades into "static site served from wherever Pages keeps your assets, re-fetched constantly." For a site with a handful of pages and nearby users, you'll never notice. For a 150-tool site with a long tail of URLs and an audience twelve time zones from your storage, every page view pays the full trip.

Three rules, one real insight

The fix was three Cache Rules: hashed static assets (/_next/static/*, fonts) cached for 30 days, sitemap and robots for a day, and a catch-all that caches HTML with a 14-day edge TTL while ignoring the origin's cache-control header — plus ignoring query strings so ?utm_source=twitter doesn't fragment the cache into a thousand near-identical copies.

Two of those rules are free wins. Next.js content-hashes everything under _next/static, so those files are immutable by construction; caching them for 30 days (or a year) carries zero risk. Query-string normalization is similarly safe for a static site and often matters more than people expect — marketing links alone can shred a cache.

The catch-all HTML rule is where the actual trade-off lives, and it's the part the write-up underplays.

The staleness tax

Cloudflare's own Pages documentation warns that Cache Rules "may lead to stale assets being served after a deployment." A 14-day edge TTL that ignores cache-control means exactly that: you deploy a fix, and the edge keeps serving the old HTML until the TTL expires or you purge. The case study's 7-day browser TTL on HTML is riskier still — you can purge Cloudflare's cache with an API call, but you cannot purge your users' browsers. A bad deploy lives in their disk cache for a week.

The saner version of this setup keeps the aggressive edge TTL, drops browser TTL on HTML back to something short (minutes, or must-revalidate), and purges on every deploy from CI:

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

That's one extra step in a deploy workflow, and it converts "stale for up to 14 days" into "stale for the seconds between deploy and purge." For a static-export site, hashed assets don't need purging at all — HTML is the only thing that goes stale, which is what makes this pattern safe here and dangerous on anything with per-user responses. If your pages vary by cookie or session, a catch-all HTML cache rule is how you end up serving one user's page to another.

China is a network problem wearing a caching costume

The 20-second LCP wasn't primarily a cache problem, and the fix didn't fully solve it — first paint from China landed at 3–5 seconds, not the sub-second numbers the rest of the world gets. That's because Cloudflare has no presence in mainland China unless you're an Enterprise customer on the China Network, which runs on JD Cloud data centers and requires an ICP filing plus content vetting. Everyone else's China traffic exits the country — with all the latency and packet loss that implies — before it reaches a Cloudflare PoP. Caching harder means fewer of those painful round trips, not shorter ones.

The other China fix in the case study is worth stealing regardless: the site loaded Sentry from its public CDN, which routinely times out from the mainland. Downloading the 70KB bundle and serving it first-party eliminated the dependency. Self-hosting third-party scripts is table stakes for a China audience — Google Fonts, analytics snippets, and most Western CDNs range from slow to blocked — and it's a good idea everywhere else too, since it removes a third-party single point of failure from your critical path.

Check yours before Monday's standup

The verdict on the technique: real, documented, and low-effort for static sites, with self-reported numbers that are plausible in direction if not universal in magnitude. The verdict on the platform: Cloudflare's defaults optimize for never serving a stale or wrong byte, which means "deployed to the edge" does not mean "served from the edge." Cache hit rate is configuration you own.

The audit takes ten seconds:

curl -sI https://yoursite.com/ | grep -i cf-cache-status

If that says DYNAMIC on a page that hasn't changed since last quarter, you're paying an origin round trip on every view — and unlike the hard parts of web performance, this one's fixable before lunch.

Sources & further reading

  1. Cloudflare Cache Hit Rate: 7% to 90% — dev.to
  2. Default cache behavior — developers.cloudflare.com
  3. Serving Pages — developers.cloudflare.com
  4. Cloudflare China Network — developers.cloudflare.com
  5. Understanding Cloudflare Caching: What Gets Cached and How to Control It — blog.derlin.ch
Emeka Okafor
Written by
Emeka Okafor · Security Editor

Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.

Discussion 5

Join the discussion

Sign in or create an account to comment and vote.

Kaidaira @kaidaira · 1 day ago

so cloudflare's default behavior requires manual cache rules for static html. that's... not ideal for the 'set it and forget it' crowd. good catch but feels like something that should default differently

Brianna Cole @burned_out_bri · 22 hours ago

the "set it and forget it" dream died in 2015, but yeah—the real gotcha is staleness. once you enable caching you're now responsible for invalidation, and Cloudflare's purge API has rate limits that'll bite you if you're doing frequent deploys. saw a team get burned hard when they thought Cache Rules meant they could stop thinking about headers entirely.

Hal Mercer @greybeard_unix · 18 hours ago

yeah, but also—you're shipping to a cdn that touches everything and expecting it to just know your intent. the 'set and forget' crowd probably shouldn't be skipping the five-minute docs read anyway.

Marco Bianchi @shipfast_marco · 1 day ago

wait, so cloudflare doesn't cache html by default? that's... insane. definitely shipping without knowing this one.

Paul Nguyen @pragmatic_paul · 1 day ago

yeah, this is the kind of thing that bites everyone once. just cache your html, move on.

Related Reading