By default Cloudflare caches static files like images, CSS and JavaScript but not HTML, so every page view still goes to your origin server. With Cloudflare Cache Rules you can cache full HTML pages at the edge, which cuts time to first byte and server load dramatically, as long as you bypass the cache for logged-in users, carts and anything personalised. This guide covers how to set that up safely, including WordPress and WooCommerce specifics and automatic purging when you publish.
How Cloudflare decides what to cache
Without any rules, Cloudflare caches responses based on file extension (images, fonts, CSS, JS and so on) and respects the origin’s Cache-Control headers. HTML is not cached by default, which is why the cf-cache-status header on your pages shows DYNAMIC.
Cache Rules, found under Caching → Cache Rules in the dashboard, replaced the older “Cache Everything” Page Rules. Each rule has a filter expression (which requests it applies to) and settings (whether they are eligible for cache, edge TTL, browser TTL, cache key options). Rules are evaluated in order, and when several match, settings from later rules can override earlier ones, so order matters.
Caching HTML at the edge with a Cache Rule
The safest pattern is one rule whose expression includes only anonymous page views. Use the expression editor rather than the visual builder, so you can combine conditions precisely. A starting point for a typical content site:
(http.host eq "example.com"
and http.request.method in {"GET" "HEAD"}
and not starts_with(http.request.uri.path, "/wp-admin")
and not starts_with(http.request.uri.path, "/api/")
and not http.request.uri.path in {"/login/" "/wp-login.php" "/account/"}
and not http.cookie contains "session_id"
and not http.cookie contains "wordpress_logged_in_")
Then set:
- Cache eligibility: Eligible for cache.
- Edge TTL: override the origin with a fixed TTL (for example a few hours), or respect origin headers if your application already sends sensible
Cache-Controlvalues for HTML. - Browser TTL: respect the origin, or keep it very short for HTML. You can purge Cloudflare’s cache, but you cannot purge a visitor’s browser cache.
The expression uses Cloudflare’s Rules language, which is based on Wireshark-style filter syntax. Cookie names in WordPress include a hash suffix, which is why the rule uses contains "wordpress_logged_in_" rather than an exact name.
Bypassing the cache for logins, carts and sessions
The cardinal rule of HTML caching: a page that varies per user must never be served from a shared cache. Otherwise one visitor can see another’s name, cart or account details. Identify every signal of personalisation:
- Cookies that indicate a logged-in or session state.
- Paths for admin, account, cart, checkout and login pages.
- Methods other than GET and HEAD.
- Authorization headers on API requests.
You can either exclude these in the caching rule’s expression (as above) or add a second rule after it, matching those conditions with “Bypass cache”. I prefer the exclusion approach for clarity, plus an explicit bypass rule for the most sensitive paths as a safety net.
Also check what your origin sends. A response that sets a cookie on every page view, for example because a plugin starts a PHP session for anonymous visitors, generally won’t be cached and is worth fixing at the origin anyway.
Query strings and the cache key
By default the full URL, including the query string, is part of the cache key. That means /pricing/?utm_source=newsletter and /pricing/ are cached separately, and every unique tracking parameter is a guaranteed cache miss.
- Bypass caching for query strings that change content per user, such as search (
?s=) or preview links. - Normalise marketing parameters where possible. Cache Rules offer cache key options such as query string handling; which options are available depends on your plan, so check the Cache Rules settings in your dashboard. A Worker can also normalise the cache key when rules alone can’t.
- Enable “Ignore query string order” where available, so
?a=1&b=2and?b=2&a=1share an entry.
Edge TTL vs browser TTL
These are easy to confuse and have very different consequences:
| Setting | Controls | Can you purge it? |
|---|---|---|
| Edge TTL | How long Cloudflare’s data centres keep the response | Yes, instantly via dashboard or API |
| Browser TTL | The Cache-Control max-age sent to visitors |
No; it lives on users’ devices until it expires |
For HTML, I use a long edge TTL with purge-on-publish and a short or zero browser TTL. For fingerprinted assets like app.3f9a1c.js, both can be long because the filename changes on every deploy.
If your origin can send separate directives for shared caches, s-maxage applies to CDNs while max-age applies to browsers:
<?php
// Anonymous HTML: cache at the edge for 4 hours, browsers revalidate.
if ( ! is_user_logged_in() && ! is_admin() ) {
header( 'Cache-Control: public, max-age=0, s-maxage=14400' );
}
Purging the cache when you publish
A long edge TTL is only safe if content updates reach visitors immediately. Purge the affected URLs via the API whenever content changes. Create an API token with the Zone → Cache Purge → Purge permission, scoped to the zone:
curl -X POST "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/purge_cache"
-H "Authorization: Bearer $CF_API_TOKEN"
-H "Content-Type: application/json"
--data '{"files":["https://example.com/","https://example.com/blog/my-post/"]}'
Purge the changed page plus pages that list it: the home page, the blog index, category archives and feeds. Purge by prefix, hostname or cache tag can be more convenient for larger changes; check Cloudflare’s docs for which purge types your plan supports. In WordPress, hook into post saves:
<?php
add_action( 'wp_after_insert_post', function ( $post_id, $post ) {
if ( 'publish' !== $post->post_status || wp_is_post_revision( $post_id ) ) {
return;
}
$urls = [
get_permalink( $post_id ),
home_url( '/' ),
get_post_type_archive_link( $post->post_type ) ?: home_url( '/blog/' ),
];
wp_remote_post(
'https://api.cloudflare.com/client/v4/zones/' . CF_ZONE_ID . '/purge_cache',
[
'headers' => [
'Authorization' => 'Bearer ' . CF_API_TOKEN,
'Content-Type' => 'application/json',
],
'body' => wp_json_encode( [ 'files' => array_values( array_filter( $urls ) ) ] ),
'blocking' => false,
]
);
}, 10, 2 );
Define CF_ZONE_ID and CF_API_TOKEN in wp-config.php, never in the theme. In Next.js or other headless setups, trigger the same API call from your CMS webhook or deploy pipeline.
Reading cache status headers
Check what Cloudflare is doing with curl, using a clean request with no cookies:
curl -sI https://example.com/blog/my-post/ | grep -iE "cf-cache-status|age|cache-control"
HIT: served from Cloudflare’s cache.MISS: not in cache yet; fetched from origin and (if eligible) stored.EXPIRED/REVALIDATED/STALE/UPDATING: cached copy existed but needed refreshing.BYPASS: a rule or origin header told Cloudflare not to cache.DYNAMIC: not considered cacheable; your rule probably didn’t match.
Request the page twice. If the second response is still MISS, look for Set-Cookie or Cache-Control: private/no-store headers from the origin. Remember that each Cloudflare data centre has its own cache, so a page can be a HIT in one location and a MISS in another.
Pitfalls with personalised pages
- Server-side personalisation for anonymous users, such as geo-based currency, “recently viewed” blocks or A/B tests rendered on the server, gets cached for everyone. Move it client-side or vary the cache key deliberately.
- CSRF tokens and nonces embedded in cached HTML can expire before the cached page does. WordPress nonces are time-limited, so a long edge TTL can break AJAX forms for logged-out visitors. Keep the TTL shorter than the nonce lifetime or fetch tokens via JavaScript.
- Redirect loops after enabling caching are often an SSL mode issue rather than caching; see my guide to fixing too many redirects with Cloudflare.
- Stacked caches (plugin cache, server cache, Cloudflare) make “my change isn’t showing” hard to debug. My WordPress cache troubleshooting guide walks through each layer.
WordPress and WooCommerce specifics
For WordPress, bypass the cache for these cookies and paths:
- Cookies:
wordpress_logged_in_,wp-postpass_,comment_author_. - Paths:
/wp-admin/(except static assets),/wp-login.php,/wp-json/if your endpoints return user-specific data, and preview URLs (?preview=true).
WooCommerce adds more:
- Cookies:
woocommerce_items_in_cart,woocommerce_cart_hash,wp_woocommerce_session_. - Paths: cart, checkout and my-account pages (use your actual slugs).
- Query strings:
add-to-cartandwc-ajaxrequests.
(http.cookie contains "wordpress_logged_in_"
or http.cookie contains "wp-postpass_"
or http.cookie contains "woocommerce_items_in_cart"
or http.cookie contains "wp_woocommerce_session_"
or starts_with(http.request.uri.path, "/cart")
or starts_with(http.request.uri.path, "/checkout")
or starts_with(http.request.uri.path, "/my-account")
or http.request.uri.query contains "add-to-cart"
or http.request.uri.query contains "wc-ajax")
Use that as a “Bypass cache” rule placed after your HTML caching rule. If you’d rather not maintain rules yourself, Cloudflare’s Automatic Platform Optimization (APO) for WordPress handles HTML caching and purging through the official plugin, at the cost of less control.
Key takeaways
- Cloudflare doesn’t cache HTML by default; a Cache Rule marking anonymous pages “eligible for cache” changes that.
- Exclude or bypass every signal of personalisation: session cookies, account and cart paths, non-GET methods.
- Use a long edge TTL with purge-on-publish, and a short browser TTL for HTML.
- Watch for tracking query strings fragmenting the cache key.
- Verify with
cf-cache-statusand look for originSet-Cookieheaders when pages refuse to cache. - For WooCommerce, cart and session cookies must always bypass the cache.
Frequently asked questions
Do Cloudflare Cache Rules work on the free plan?
Yes, Cache Rules are available on all plans, though the number of rules and some cache key options vary by plan. Check the Cache Rules page in your dashboard for your limits.
Why does cf-cache-status show DYNAMIC on my pages?
DYNAMIC means Cloudflare didn’t consider the response cacheable, which for HTML usually means no Cache Rule matched the request. Check the rule expression and hostname, then test with a request that has no cookies.
Is it safe to cache HTML on a WooCommerce store?
It is safe for anonymous browsing of product and category pages if you bypass cart, checkout and account pages and all WooCommerce session cookies. Test carefully with a fresh browser session after adding items to the cart.
How do I clear Cloudflare’s cache automatically after publishing?
Call the purge_cache API with the changed URLs whenever content is published, using a scoped API token. In WordPress, a hook such as wp_after_insert_post or the official Cloudflare plugin can do this for you.
If you want edge caching set up for a WordPress, WooCommerce or headless site without risking cached carts or logins, contact me and I can configure and test it for you.