A static build decides what HTML exists before any request arrives. It does not
decide whether Cloudflare keeps a copy of that HTML at the edge. On Cloudflare
Pages the default is that it does not, and a page on a fully static site answers
cf-cache-status: DYNAMIC unless someone has changed that.
It is easy to treat “static” and “cached” as the same word. They name two different systems, and the gap between them usually surfaces the same way: a site audit flags a page as slow, the page is fast every time someone checks it by hand, and nobody can reproduce the number.
What output: 'static' actually controls
Take Astro as the example. Its output: 'static' prerenders every page at build
time unless a route opts out.[1] That is a build instruction, and
what it produces is files. What happens to those files in transit (which network
holds a copy, for how long, and whether a given request gets served from it) is
a separate system with its own configuration. The word “static” in the build
setting does not reach into it.
The same is true of every static-site generator. They build files. The host decides what gets cached.
What the check shows
A request for any HTML page on a default Pages deployment comes back with two revealing headers:
$ curl -sI https://example.com/blog/some-post/ | grep -i '^cache-control\|^cf-cache-status'
cache-control: public, max-age=0, must-revalidate
cf-cache-status: DYNAMICAsk the same site for an image on that page and the answer is different:
$ curl -sI https://example.com/images/hero.png | grep -i '^cache-control\|^age\|^cf-cache-status'
cache-control: public, max-age=604800
age: 1354
cf-cache-status: HITThe image is cached, the stylesheet is cache-eligible and warms up to HIT
under traffic, and the HTML is the one thing on the page that is never eligible
at all. A page like that is usually quick when warm. Fetched from a location
that has no warm copy, it is slower, and a single-sample crawler is exactly the
tool that lands on a bad draw and reports it.
”Static” and “cached” are different systems
Cloudflare’s CDN does not cache HTML by default.[2] That holds on Pages and everywhere else. HTML and JSON sit outside the default cacheable set, and everything else a normal page pulls in (CSS, JS, fonts, images) sits inside it. So on a “fully static” site the assets get a CDN cache and the documents do not.
DYNAMIC is the status Cloudflare returns when it decides, at request time,
that a response is not eligible for cache and skips the cache lookup
entirely.[3] Pages then serves the request directly. Pages is not a
single origin server: uploaded assets sit in a tiered store with a one-week TTL
and can be evicted from any given location at any time.[4] So a
DYNAMIC HTML response is usually still quick, because it is a read from a store
near the user rather than a trip back to one server. But it is a read, not a
hit. A location without a warm copy does that read cold, and that is the number
a crawler reports.
Pages also states the policy in the response itself: every asset carries
Cache-Control: public, max-age=0, must-revalidate.[4] A common
override gives fingerprinted build assets a year, since their filenames change
whenever their bytes do. Left alone, HTML keeps max-age=0, must-revalidate,
which tells browsers and every proxy in between to keep nothing and check every
time.
What buying the host covered, and what it did not
The case for a managed static host is that delivery stops being your problem,
and most of it does. You stop running and patching a web server, and you get
TLS, global distribution, DDoS absorption and atomic deploys for the price of a
git push. For most marketing sites that is a good trade.
What you keep is the cache policy for your own HTML, and nobody hands it to you. The default makes sure a visitor never sees a stale page, and to do that it gives up serving any page from cache. The trade stays invisible until something measures it, because “static hosting” sounds like caching is already settled. What the host settled is distribution. Caching the documents themselves is still a decision, and until you make it, the platform has made it for you, on the conservative side.
Turning it on, and what that costs
Making HTML eligible for the edge cache takes a Cache Rule: mark the response
cacheable and give it an edge TTL. That is the supported way to cache something
Cloudflare would otherwise skip.[5] For a marketing site, a short
edge TTL plus stale-while-revalidate is the setup that fits. A few minutes of
edge TTL turns repeat requests into a HIT, and a day of
stale-while-revalidate covers the moment while a fresh copy is fetched in the
background.
The cost lands on deploys. HTML paths stay the same across releases
(/blog/x/ is the same URL before and after), so once the edge holds a copy, a
deploy’s change to that page stays invisible until the TTL runs out or you
purge. Fingerprinted assets avoid this because their filenames change with their
bytes, and HTML gets no such protection. Edge-caching HTML therefore makes every
deploy depend on a prompt purge, and a forgotten purge looks exactly like a
deploy that silently failed.
The second cost is scope. A blanket rule is wrong on the day the site gains a
route it should never have touched. The _headers file is not applied to
responses from Pages Functions,[6] so on a site where some pages are
generated by Functions, a rule scoped through _headers skips them
automatically. A zone-level Cache Rule matching /* would not, and the first
personalised or authenticated HTML route added later would be served out of a
shared cache. Whichever tool you use, the rule has to name exactly what it
covers.
Leaving it off is also a legitimate answer. If the site is already fast enough that a cold read costs a couple of hundred milliseconds, and the list of things that would actually move the business is long, this may not be near the top of it. What matters is choosing that, not inheriting it.
The check, for your own domain
| You get by default | You turn on yourself | How you would know it is off |
|---|---|---|
| CDN cache for images, CSS, JS, fonts | CDN cache for HTML, via a Cache Rule marking it eligible with an edge TTL | curl -sI shows cf-cache-status: DYNAMIC on a page and HIT on its images |
public, max-age=0, must-revalidate on every asset | A longer max-age on fingerprinted assets, via _headers | fingerprinted build paths still answer max-age=0 |
| Global distribution, TLS, DDoS absorption | Nothing, this is the part the host genuinely owns | not applicable |
| A near-user store read for uncached HTML | A cache HIT for HTML | no Age header on a page response, ever |
One line answers it for any domain:
curl -sI https://yourdomain/ | grep -i cf-cache-status. If it says DYNAMIC
and you had assumed a static site was a cached one, it is not, and that is the
default rather than a mistake. Whether it matters is a separate question, so
measure the cold time to first byte from a few regions before you spend a change
on it. That decision should be yours, not one the platform default made for you
by leaving it alone.
Sources
- Configuration Reference
Supports: Astro's output: 'static' prerenders every page at build time unless a route opts out; it is a build-time setting and does not govern how the resulting HTML is cached in transit.
- Default Cache Behavior
Supports: The Cloudflare CDN does not cache HTML by default; static file types such as CSS, JS, fonts and images are in the default cacheable set and HTML is not.
- Cloudflare cache responses
Supports: A cf-cache-status of DYNAMIC means Cloudflare determined at request time that the response is not eligible for cache and sent the request on without a cache lookup; HIT means it was served from Cloudflare's cache.
- Serving Pages
Supports: Cloudflare Pages serves cacheable asset responses with Cache-Control: public, max-age=0, must-revalidate, and uploaded assets have a one-week TTL and can be evicted from a location at any time.
- Cache Rules
Supports: Cache Rules adjust what is eligible to cache, how long it is cached, and where, which is the supported way to cache a response type Cloudflare would not cache by default.
- Headers
Supports: The _headers file overrides response headers on static asset responses but is not applied to responses generated by Pages Functions.