Cloudflare full-page caching for WordPress & WooCommerce: the complete setup
Put Cloudflare in front of a WordPress site and your images, CSS and JavaScript get
cached at the edge — but your HTML doesn't. Every page view still travels to your
origin, boots PHP, runs every plugin hook and queries MySQL. Run any WordPress URL
through our cache checker and you'll almost certainly see
cf-cache-status: DYNAMIC — Cloudflare's
way of saying "I'm not even trying to cache this."
This guide sets up full-page (HTML) caching with Cloudflare Cache Rules, properly: the exact bypass expressions for WordPress and WooCommerce, the rule order that most guides get backwards, and the part almost everyone skips — what Edge TTL and Browser TTL actually control, and why mixing them up means stale pages you can't purge.
First, understand the two caches you're configuring
A Cache Rule sets two separate lifetimes, and they live in completely different places:
| Edge cache (Edge TTL) | Browser cache (Browser TTL) | |
|---|---|---|
| Lives | In Cloudflare's 300+ data centers | On each visitor's device |
| Serves | Every visitor near that location | Only that one visitor, on repeat views |
| You can purge it | Yes — one click or API call | No — it expires when it expires |
| Sane value for HTML | Hours to days (aggressive) | Minutes, or 0 (conservative) |
This asymmetry is the whole strategy: be aggressive at the edge, conservative
in the browser. You can purge Cloudflare's cache the moment you publish an
update — so a long Edge TTL costs you nothing in freshness. You cannot reach
into a visitor's browser: if you let browsers cache your HTML for a day, they'll show
yesterday's page for a day, no matter how often you purge Cloudflare. Long
TTLs belong to fingerprinted assets
(app.a3f9c2.css) — never to HTML.
The setup: two Cache Rules
In the Cloudflare dashboard, go to Caching → Cache Rules. We'll create two rules — one that makes everything cacheable, one that carves out what must never be cached.
Rule 1: cache everything on the domain
Expression:
(http.host eq "example.com")
Settings:
- Cache eligibility: Eligible for cache
- Edge TTL: Ignore cache-control header and use this TTL → 4 hours (raise it to a day or more once purge-on-publish works — see below)
- Browser TTL: Respect origin (WordPress sends
Cache-Control: max-age=0for HTML by default, which is exactly what browsers should see)
Why override the origin header for the edge but respect it for the browser? Because
WordPress's default Cache-Control
says "don't cache me" — correct for browsers you can't purge, wasteful for an edge you
can. The Edge TTL override lets Cloudflare cache what the origin header forbids,
without changing what browsers are told.
Rule 2: bypass everything dynamic (this rule must come after Rule 1)
Expression — WordPress core plus WooCommerce:
(starts_with(http.request.uri.path, "/wp-admin"))
or (http.request.uri.path contains "/wp-login.php")
or (http.request.uri.path contains "/cart")
or (http.request.uri.path contains "/checkout")
or (http.request.uri.path contains "/my-account")
or (http.request.uri.query contains "wc-ajax=")
or (http.cookie contains "wordpress_logged_in_")
or (http.cookie contains "wordpress_sec_")
or (http.cookie contains "wp_woocommerce_session_")
or (http.cookie contains "woocommerce_items_in_cart")
or (http.cookie contains "woocommerce_cart_hash")
or (http.cookie contains "comment_author_")
or (http.cookie contains "PHPSESSID")
Setting: Cache eligibility → Bypass cache.
What each group does:
- Paths — admin, login, and the three WooCommerce pages that are different for every visitor. This is defense in depth: the cookie conditions below are the primary protection, the paths catch anything that slips through.
wc-ajax=— WooCommerce's AJAX endpoints (?wc-ajax=get_refreshed_fragmentsand friends) that keep the mini-cart live. Cache one of these and every visitor sees the same cart count.- Login cookies (
wordpress_logged_in_,wordpress_sec_) — logged-in users see admin bars, previews and personalized content. One cached admin-bar page served to anonymous visitors is the classic full-page-caching horror story. - Session cookies (
wp_woocommerce_session_,woocommerce_items_in_cart,woocommerce_cart_hash) — set the moment someone adds an item to their cart. From that second on, that visitor must bypass the cache on every page, or they'll see pages rendered for someone else's session. This is a data-leak risk, not just a UX bug. comment_author_,PHPSESSID— commenters with prefilled forms, and any plugin that starts PHP sessions.
The rule-order mistake half the internet makes: Cloudflare evaluates Cache Rules top to bottom, and when rules conflict, the last matching rule wins — it is not "first match, then stop." Your bypass rule therefore belongs after the cache-everything rule, so it overrides it. Guides that tell you "bypass first, cache-everything second" have it exactly backwards: in that order, the cache-everything rule matches last and wins, and your checkout page ends up cached. If you only remember one thing from this article, make it this.
Optional Rule 3: long TTLs for feeds and sitemaps
Feeds and XML sitemaps are generated by PHP too, and bots hammer them. They're safe
to cache — just keep the TTL short enough that new posts show up (an hour is plenty).
If you use a dynamic sitemap plugin that must always be fresh, bypass it instead;
Servebolt's variant of this guide excludes .xml and .txt
outright, which is the cautious default if you don't want to think about it.
The Set-Cookie trap (why your cache never gets warm)
By default, Cloudflare refuses to cache any response that carries a
Set-Cookie header. A surprising number of WordPress themes and plugins
start a session or set a cookie on every page view — analytics plugins,
GeoIP plugins, some page builders, WooCommerce configured to start sessions early.
Result: every response has Set-Cookie, nothing is ever stored, and your
hit ratio sits at zero even though your rules are correct.
Two things to know:
- When a Cache Rule sets an Edge TTL override, Cloudflare strips the
Set-Cookieheader and caches anyway. That makes the cache work — but if that cookie mattered (a session!), you've just cached a session-bound page. The bypass cookie list above is what keeps this safe. - Better: find what's setting cookies for anonymous visitors and stop it. Open an incognito tab, load a page, and check the response headers. An anonymous product page should set no cookies until "add to cart" is clicked.
Verify it — properly, from more than one place
Test the full matrix, because each state exercises a different rule:
| Request | Expected cf-cache-status |
|---|---|
| Product/blog page, incognito, 1st request | MISS (edge stored it) |
| Same page, 2nd request | HIT |
| Same page, with an item in the cart | BYPASS (cookie rule) |
| /checkout/, /cart/, /my-account/ | BYPASS |
| Logged in, any page | BYPASS |
Full decoder for every status value in
our cf-cache-status guide. And remember that a
HIT from your desk only proves your nearest Cloudflare location
is warm — each PoP caches independently. The
multi-location cache checker shows the status from
several continents at once, and the website speed
test shows what the first vs. repeat visit actually costs per region.
Purging: wire it up before you raise the TTL
Full-page caching without purge-on-publish means editors see stale pages and panic. Set it up on day one:
- The official Cloudflare WordPress plugin or Super Page Cache purges automatically when you publish or update content — ideally just the changed URLs, not everything.
- Avoid "purge everything on every save" configurations: one edit and your whole site is cold at every edge location — we've measured what that does to real traffic.
- Remember the asymmetry from the top: purge clears the edge only. Whatever Browser TTL you set keeps living in visitors' browsers — which is why HTML gets minutes there, not days.
Cache Rules vs. APO — which one?
Cloudflare's Automatic Platform Optimization (APO) is the managed alternative: a Workers-based service ($5/month on the Free plan, included from Pro up) that caches WordPress HTML with built-in WordPress/WooCommerce cookie logic and purges via the official plugin on publish. Its standout feature: pages are replicated to Workers KV within about a minute, globally — one visit anywhere seeds every location, whereas a Cache Rule only fills the one PoP that got the request.
| Cache Rules (this guide) | APO | |
|---|---|---|
| Cost | Free, all plans | $5/mo on Free, included Pro+ |
| Control | Full — your expressions | Black box, WordPress-aware |
| WooCommerce exclusions | You maintain them | Built in |
| Cold-start behavior | Per-PoP: each location warms separately | KV replication softens it |
| Non-WordPress paths on same zone | Handled by your rules | Not covered |
Never stack both. If APO is on, a manual cache-everything rule overrides APO's cookie logic and reintroduces the cached-cart problem APO exists to prevent. Pick one approach.
After the setup: the cache is configured, not warm
Everything above makes your HTML cacheable. It doesn't put a single page into a single cache — that still takes a request per URL, per edge location. After every purge and every TTL expiry, all of it starts cold again, and eviction quietly empties low-traffic locations even sooner. Your page-cache plugin's preload can't help here: it warms your server, not Cloudflare's 300+ locations.
That last mile is what warmup.rocks does: it reads your sitemap and requests every URL through proxies around the world on a schedule, so each edge location holds its own warm copy — with per-location hit-ratio reporting so you can see it working.
Does Cloudflare cache WordPress HTML by default?
No. By default Cloudflare caches static assets (images, CSS, JavaScript) based on
file extension, but never HTML — every page view goes to your origin and shows
cf-cache-status: DYNAMIC. You must opt in with a Cache Rule set to
"Eligible for cache" (or use APO).
What's the difference between Edge TTL and Browser TTL?
Edge TTL controls how long Cloudflare's data centers keep a copy; you can purge it anytime, so it can be long. Browser TTL controls how long a visitor's browser keeps a copy; you can never purge it, so for HTML it should be minutes or zero. Purging Cloudflare does not touch what's already in visitors' browsers.
Why is my checkout page cached even though I created a bypass rule?
Almost always rule order. Cloudflare combines matching Cache Rules and conflicting settings take the value of the last matching rule. If your bypass rule sits above the cache-everything rule, the cache-everything rule matches last and wins. Move the bypass rule below it and purge everything once.
Is it safe to cache HTML for a WooCommerce store?
Yes — for the pages that are identical for every anonymous visitor: products,
categories, content. Cart, checkout and account pages must bypass, and so must any
request carrying WooCommerce session cookies (wp_woocommerce_session_,
woocommerce_items_in_cart, woocommerce_cart_hash). The
mini-cart stays live because WooCommerce fetches it via wc-ajax
fragments, which your rules exclude from caching.
Should I use APO or Cache Rules?
APO if you want managed, WordPress-aware caching with global KV replication and don't need custom control. Cache Rules if you want full control over expressions and TTLs, run non-WordPress paths on the same zone, or don't want the $5/month on the Free plan. Never run both together.
Configured the cache? Now keep it warm.
warmup.rocks reads your sitemap and keeps your WordPress HTML warm across Cloudflare's edge locations — with per-location hit-ratio reporting after every run. See how our WordPress cache warmer works.
Start your 7-day free trial