The tracking script that 109 of 114 pages load is 7,850 bytes, sits behind the same CDN, and answers with an ETag, a Cache-Control: max-age=43200, and cf-cache-status: HIT. The pages that load it answer with Last-Modified and nothing else — no ETag, no Cache-Control, and cf-cache-status: DYNAMIC on every single one.

I audited the home page of all 114 sites in one content network. 113 of them send no Cache-Control header at all. One sends public, max-age=300. Zero send an ETag. All 114 send Last-Modified, all 114 keep that value stable across two requests, and all 114 answer a conditional request with 304 and a zero-byte body.

So the plumbing works. What's missing is the decision — how long this response may be reused without asking anyone. Nobody on any of these 114 hosts has made that decision, and the browser is quietly making it instead.

What I measured

For each of the 114 hosts, over plain urllib, no headless browser and no Lighthouse:

  1. One GET / with Accept-Encoding: gzip, deflate, br, recording status, time to first byte, wire bytes, and the cache-relevant response headers.
  2. A second GET / to compare Last-Modified — a validator that changes between two requests five seconds apart is worse than no validator.
  3. A third GET / carrying If-None-Match and If-Modified-Since built from the first response.
  4. Up to five <link rel="stylesheet"> and <script src> URLs from each page, fetched for their own cache policy.

The pages are static documents of a few hundred words, the kind of thing that should be the easiest caching problem on the internet: an explainer on how prediction markets price an event, a fee schedule that changes once a year, a market page for a contract that does not settle until 2027. Content that is expensive to regenerate and cheap to store.

The audit is short

The whole conditional-request test fits in a function. The only subtle part is not sending Accept-Encoding when you want to parse the HTML you just downloaded — I got br back, tried to regex it as text, and got zero assets out of 114 pages on the first pass.

import urllib.request, email.utils, time UA = "Mozilla/5.0 (compatible; cache-audit/1.0)" def get(url, extra=None): req = urllib.request.Request(url, headers={ "User-Agent": UA, "Accept-Encoding": "gzip, deflate, br", # drop this if you need the body as text  }) for k, v in (extra or {}).items(): req.add_header(k, v) t0 = time.time() with urllib.request.urlopen(req, timeout=30) as r: body = r.read() return { "status": r.status, "ttfb_ms": int((time.time() - t0) * 1000), # headers received, body not  "bytes": len(body), "h": {k.lower(): v for k, v in r.headers.items()}, } def audit(url): a = get(url) cond = {} if a["h"].get("etag"): cond["If-None-Match"] = a["h"]["etag"] if a["h"].get("last-modified"): cond["If-Modified-Since"] = a["h"]["last-modified"] b = get(url, cond) if cond else None return { "cache_control": a["h"].get("cache-control"), "etag": a["h"].get("etag"), "last_modified": a["h"].get("last-modified"), "cf_cache_status": a["h"].get("cf-cache-status"), "first_ttfb": a["ttfb_ms"], "cond_status": b["status"] if b else None, "cond_ttfb": b["ttfb_ms"] if b else None, "cond_bytes": b["bytes"] if b else None, } 

Run that against a page and the answer comes back in one line: {"cache_control": None, "etag": None, "last_modified": "Sat, 31 Oct 2026 11:09:50 GMT", "cf_cache_status": "DYNAMIC", "first_ttfb": 1498, "cond_status": 304, ...}.

What the 114 pages sent

Header Pages sending it
Last-Modified 114 / 114
Cache-Control 1 / 114
ETag 0 / 114
cf-cache-status: DYNAMIC 114 / 114
Content-Encoding: br 114 / 114

Three separate layers, three different verdicts.

Compression is fine. Every page came back brotli-encoded with Vary: Accept-Encoding, and the median raw HTML of 73,641 bytes arrived as 10,578 bytes on the wire. That's an 86% cut, applied uniformly, with no exceptions to hunt down.

Validation is fine, and better than fine. 114 out of 114 answered 304 with a body of exactly zero bytes. Not one page re-sent a 10 KB document to confirm it hadn't changed. Whatever else is wrong here, the machine that decides "unchanged" is working.

Freshness is absent. One page out of 114 said how long its response stays valid. The other 113 handed the browser a validator and no policy, which per RFC 9111 §4.2.2 means the browser is allowed to invent one.

A 304 is cheap in bytes and expensive in time

Here is the part that surprised me. The 304 responses came back in a median of 1,391 ms. The full responses came back in a median of 1,467 ms — a difference of 86 ms, about 6%.

The 304 saved 10,578 bytes of compressed transfer and almost no time. That makes sense once you see where the 304 came from: cf-cache-status: DYNAMIC on all 114 pages means the CDN did not have the page, did not store it, and forwarded the request to the origin. The origin is what decides the page is unchanged, and the origin is what takes 1.4 seconds.

A conditional request that has to reach the origin inherits the origin's latency. You saved bandwidth on a document that was already compressed to 10 KB, and you paid the full round trip to do it.

Absolute numbers here are from a single vantage point in Asia against a network whose origin is elsewhere, so treat the milliseconds as directional. The header values are not: DYNAMIC on 114 of 114 is a configuration fact, not a network measurement.

The same network gets the assets right

The tracker at coinpriceoracle.com/t.js is loaded by 109 of these 114 pages, cross-domain, on a different host than the one serving the page. It gets:

HTTP/1.1 200 OK cache-control: max-age=43200 etag: W/"6a79412e-1eaa" last-modified: Mon, 10 Aug 2026 03:10:38 GMT cf-cache-status: HIT age: 2118 

An ETag, a 12-hour TTL, an edge HIT with a 35-minute age, 7,850 bytes. Every one of those headers is correct for a file that gets deployed rather than generated.

Across 407 stylesheet and script samples from the same 114 sites: 292 served from the edge cache, 63 MISS, 39 REVALIDATED, one DYNAMIC. The asset layer is configured. The document layer was never touched.

The 12-hour TTL on filenames that never change

Of those 407 asset samples, 358 — 88% — carry max-age=43200. That's twelve hours, and it is the correct default only if the URL changes when the bytes change.

These URLs do not change. They look like /site.css, /all.min.css, /remixicon.min.css, /event-reporting.min.js, /t.js. Exactly two of the 407 filenames contained a content hash, both from a Next.js build that ships /_next/static/chunks/… whether you ask it to or not.

Twelve hours on an un-versioned filename is a specific kind of bug, and it's the kind you only notice from the other side. Ship a CSS fix at 09:00 and a visitor who loaded the site at 08:50 keeps the old stylesheet until 20:50 — same URL, still fresh in their cache, no request sent, no way for them to know. The fix is live and invisible. With immutable on top of a hashed name, none of that happens: new bytes mean a new URL, so a one-year TTL is free.

The inverse is also true: if the URL is stable, the TTL has to be short, and then you need stale-while-revalidate so the short TTL doesn't turn into a blocking round trip on every deploy day. A page like this comparison of Polymarket's fee tiers is regenerated whenever the template changes and edited maybe twice a year. It is the textbook case for a five-minute TTL with a one-day stale window: nobody notices the five minutes, and nobody waits for the origin.

"Heuristic caching" is the browser guessing on your behalf

With no Cache-Control, RFC 9111 lets a cache assign freshness heuristically, and the classic implementation is 10% of the time since Last-Modified.

The median Last-Modified age across these 114 pages was 6.7 days, so the heuristic lands around 16 hours. Comfortable. But the value is not a policy, it's arithmetic on a timestamp the deployment wrote — and it moves with the content:

  • The most recently deployed page in the set had a Last-Modified of 0.99 days old. Its heuristic freshness is about 2.4 hours.
  • 37 of 114 pages carried a Last-Modified older than 30 days, which the heuristic happily turns into three days of staleness.
  • Right after a deploy, Last-Modified is seconds old and the heuristic window collapses to seconds. The site is least cacheable exactly when a release has just put it under load.

And the heuristic can't tell the difference between those cases. A page whose content is pinned to a settlement date — the BTC 150k Q1 2027 contract page — gets the same arithmetic as a page rebuilt hourly, because the only input is a timestamp that says nothing about whether the bytes will change.

None of these numbers were chosen by anyone. They're side effects of a timestamp, interpreted by a spec clause that exists so caches have something to do when the origin declines to say.

What I would send instead

Three policies, chosen by one question: can this URL's content change without the URL changing?

from urllib.parse import urlparse HASHED = ("/_next/static/", "/assets/", "/static/") def cache_policy(path: str, has_hash: bool) -> str: if has_hash: # bytes cannot change without the URL changing  return "public, max-age=31536000, immutable" ext = urlparse(path).path.rsplit(".", 1)[-1].lower() if ext in ("css", "js", "woff2", "svg", "png", "webp", "avif"): # stable filename: short TTL, serve stale while revalidating  return "public, max-age=300, stale-while-revalidate=86400" # HTML: let the edge hold a short copy, keep browsers revalidating  return "public, max-age=0, s-maxage=60, stale-while-revalidate=86400" HEADERS = { "html": "public, max-age=0, s-maxage=60, stale-while-revalidate=86400", "hashed_asset": "public, max-age=31536000, immutable", "stable_asset": "public, max-age=300, stale-while-revalidate=86400", } 

The two numbers doing the work are s-maxage and stale-while-revalidate. s-maxage=60 is what turns cf-cache-status: DYNAMIC into a HIT: the edge may hold the page for a minute and serve it to the next hundred visitors without touching the origin. stale-while-revalidate=86400 means that a day past expiry, the edge still answers instantly from the stale copy and refreshes in the background — so the 1,391 ms revalidation happens off the critical path instead of inside a navigation.

ETag matters more than it looks here. Last-Modified has one-second resolution, so two deploys inside the same second produce the same validator and a client can be told "unchanged" about bytes that changed. A content hash has no such failure mode, and it's cheaper than it sounds: the body is already in memory when you're setting headers.

On Cloudflare specifically, none of this requires touching the application. A Cache Rule that matches HTML and sets s-maxage plus stale-while-revalidate gets the edge half; the ETag still has to come from the origin, because the edge will not invent one for a response it forwards.

What this does not prove

I forced the conditional request. Whether a real browser sends one at all depends on the heuristic freshness it computed for itself — which, as above, is a number nobody chose. So the honest claim is not "every returning visitor costs you 1.4 seconds." It's "whenever a browser or an edge decides to check, the check costs a full origin round trip, and nothing in the response tells it how long to wait before checking again."

I also didn't measure whether any of this affects rankings, and I'm not going to imply it does. Core Web Vitals measure what a visitor experiences, and a visitor who has never been to the site before pays the origin latency regardless of cache headers. What the headers change is the second visit, the hundredth visitor, and what happens to your origin on the morning you deploy.

The build that would have caught all of it is nine lines in CI: request each page, assert cache-control is present, assert etag or last-modified exists, assert the conditional request returns 304, and assert every hashed asset has a TTL over a year. Four assertions, run against staging, before the config goes out. The 114 pages above would have failed the first one 113 times.