---
title: "Cloudflare full-page caching for WordPress & WooCommerce: the complete setup — warmup.rocks"
description: "Cloudflare doesn't cache WordPress HTML by default. The complete Cache Rules setup: exact bypass expressions for WordPress and WooCommerce, why rule order works backwards from what most guides say, Edge TTL vs. Browser TTL, the Set-Cookie trap, and APO vs. Cache Rules."
canonical: https://warmup.rocks/blog/cloudflare-html-caching-wordpress
---

WordPress

# Cloudflare full-page caching for WordPress & WooCommerce: the complete setup

July 2026 · 12 min read · [← All posts](https://warmup.rocks/blog/)

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](https://warmup.rocks/cache-checker) and you'll almost certainly see [`cf-cache-status: DYNAMIC`](https://warmup.rocks/blog/cf-cache-status) — 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](https://warmup.rocks/glossary/point-of-presence) | 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](https://warmup.rocks/glossary/ttl) 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=0` for 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`](https://warmup.rocks/glossary/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_fragments` and 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-Cookie` header 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](https://warmup.rocks/blog/cf-cache-status). And remember that a `HIT` from your desk only proves _your nearest_ Cloudflare location is warm — [each PoP caches independently](https://warmup.rocks/glossary/point-of-presence). The [multi-location cache checker](https://warmup.rocks/cache-checker) shows the status from several continents at once, and the [website speed test](https://warmup.rocks/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](https://warmup.rocks/blog/cdn-cache-miss-data).
-   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](https://www.cloudflare.com/application-services/products/automatic-platform-optimization/)) 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](https://warmup.rocks/glossary/cache-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](https://warmup.rocks/blog/wordpress-cache-warming).

That last mile is what [warmup.rocks](https://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](https://warmup.rocks/glossary/cache-hit-ratio) reporting so you can see it working.

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).

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.

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.

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.

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.
