DEV Community

Oleksandr Kuryzhev
Oleksandr Kuryzhev

Posted on Originally published at kuryzhev.cloud

Cloudflare cache bypass mistakes on dynamic WordPress paths

Originally published on kuryzhev.cloud


A WordPress site behind Cloudflare starts serving the wrong cart contents to different visitors, or the admin dashboard flashes a cached homepage instead of the login form. Both symptoms can trace back to the same root cause: a Cloudflare cache bypass rule that doesn't actually cover every dynamic path it needs to. This is a commonly reported misconfiguration on WordPress installs sitting behind Cloudflare's proxy, and it often surfaces after someone widens caching to raise the hit ratio and then starts trusting the cache more than the rules justify.

WordPress generates a mix of static and dynamic content from the same domain — product pages that can be cached for hours next to cart, checkout, and account pages that must never be cached. Cloudflare's default cache behavior, combined with Cache Rules, does not know this distinction unless it's told explicitly. Cloudflare's documented default is to cache based on a list of file extensions and a few related heuristics, which is not enough for a CMS where nearly every page resolves through index.php.

Context

Cloudflare sits in front of the origin as a reverse proxy. By default it caches static assets — images, CSS, JS — matched by extension, while HTML responses are not cached by default because HTML is not in that default extension list. That default is the only thing standing between a dynamic WordPress page and the shared edge cache, and any rule that overrides it takes on the job of distinguishing personalized responses from public ones. WordPress complicates this because plugins like WooCommerce, membership systems, and custom REST endpoints all serve dynamic HTML from paths that look, to a naive rule, indistinguishable from static pages.

The operational goal is usually stated simply: cache everything cacheable, bypass everything session-specific. In practice this requires enumerating every dynamic path pattern a WordPress install can produce, and that list grows as plugins are added. A cache rule written during initial setup rarely gets revisited when a new plugin introduces its own dynamic endpoint, and that gap is a plausible origin for each of the failure modes below. Cloudflare's Cache Rules documentation (developers.cloudflare.com/cache/how-to/cache-rules) is the authoritative reference for expression syntax and precedence, and it's worth reading before assuming a rule behaves the way its name suggests. Note also that legacy Page Rules are deprecated for new configuration; Cache Rules are the current mechanism, and mixing both makes precedence harder to reason about.

Common failure 1: bypass rules that only match by URL path, not by cookie

A typical setup writes a Cache Rule matching http.request.uri.path contains "/cart" or similar and calls it done. This catches the obvious checkout flow but misses the broader problem: WooCommerce and most session-aware plugins set cookies — for example woocommerce_cart_hash or wp_woocommerce_session_ — that indicate a personalized response regardless of URL. If the homepage or a product page renders differently based on such a cookie, showing "3 items in cart" in a header, and the cache rule doesn't account for it, a cached response can serve one visitor's cart state to another.

The fix is to bypass cache when specific cookies are present, not just when specific paths are requested. A Cache Rule expression combining both conditions is more resilient:

(http.request.uri.path contains "/cart") or
(http.request.uri.path contains "/checkout") or
(http.request.uri.path contains "/my-account") or
(http.cookie contains "woocommerce_cart_hash") or
(http.cookie contains "wp_woocommerce_session_") or
(http.cookie contains "wordpress_logged_in_")

Set that rule's cache eligibility to Bypass cache. The exact cookie names depend on plugin versions and any prefix customization, so confirm them in browser devtools against the actual site rather than copying a list.

Watch out for: the number of Cache Rules per zone and the maximum expression length are plan-dependent. Verify current limits against the Cache Rules limits documentation before assuming an expression this long will save on a given plan.

Common failure 2: caching REST API and admin-ajax responses meant to be dynamic

A second recurring pattern involves overly broad "cache everything" rules applied at the zone level, often introduced to fix a low cache hit ratio. These rules can sweep up /wp-json/ and /wp-admin/admin-ajax.php, both of which WordPress uses constantly for dynamic operations — form submissions, live search, cart updates, nonce validation. Caching admin-ajax responses can cause a stale nonce to be served repeatedly, which may manifest as forms failing with a security-check error that has nothing to do with the plugin logic itself.

The safer pattern is to exclude these paths explicitly with a dedicated bypass rule:

(http.request.uri.path contains "/wp-json/") or
(http.request.uri.path contains "/wp-admin/") or
(http.request.uri.path eq "/wp-cron.php") or
(http.request.uri.path eq "/wp-admin/admin-ajax.php")

The action for that rule is cache eligibility Bypass cache. The explicit admin-ajax.php condition is redundant while the /wp-admin/ condition is present, but it is worth keeping if someone later narrows the admin path match.

Watch out for: rule order matters, and not in the direction people usually assume. Cloudflare evaluates all matching Cache Rules in list order, and for a given setting the last matching rule wins. So a broad "cache everything" rule placed below a bypass rule will override the bypass for overlapping requests. Put the narrow bypass rules after the broad rule, or scope the broad rule so it cannot match dynamic paths at all. Confirm evaluation order and the resulting effective settings in the dashboard's rule list rather than inferring them from rule names.

Common failure 3: relying on page-level exclusions instead of edge cache TTL discipline

A third failure mode is subtler and shows up over time rather than immediately. A site starts with careful path-based bypass rules, but as traffic grows, someone adds a blanket cache-everything rule with a long Edge Cache TTL to reduce origin load, intending it to apply only to static assets. Without respecting the Cache-Control headers WordPress and plugins already send, an aggressive fixed TTL can override origin intent for paths nobody meant to include.

This is where respecting existing headers matters. Many WordPress caching plugins (W3 Total Cache, WP Rocket, LiteSpeed Cache) send Cache-Control headers distinguishing dynamic from static responses. A Cloudflare rule that overrides those headers with a flat Edge TTL defeats that origin-level logic.

# Origin-side nginx snippet for a dynamic WordPress endpoint.
# Equivalent headers can be emitted from PHP instead, but not via
# nginx directives placed in functions.php.
location = /wp-admin/admin-ajax.php {
    add_header Cache-Control "no-store, no-cache, must-revalidate" always;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

The Cloudflare-side counterpart is a Cache Rule that sets cache eligibility to Eligible for cache and sets Edge TTL to "Use cache-control header if present, use default Cloudflare caching behavior if not", rather than a fixed override value. Check the default cache behavior documentation for which headers Cloudflare honors and when a fixed TTL still wins regardless of origin headers — this detail has shifted across Cloudflare product iterations, so re-check current docs rather than assuming legacy Page Rules behavior applies to Cache Rules.

Safer operating pattern

A more durable Cloudflare cache bypass strategy for WordPress treats dynamic path exclusion as a maintained list, not a one-time setup task. Every new plugin that introduces a checkout flow, a membership gate, or a custom REST endpoint is a candidate for a new bypass condition, and the review is cheaper at plugin install time than after a symptom appears.

Checklist before enabling broader Cloudflare caching on WordPress:
1. List all cookie names set by session-aware plugins (cart, auth, membership)
2. Confirm /wp-json/, /wp-admin/, /wp-cron.php are explicitly bypassed
3. Verify rule order and effective settings: for a given setting the last
   matching Cache Rule wins, so scope or order the broad rule accordingly
4. Check Edge TTL setting: respect origin cache-control vs fixed override
5. Test with two separate browser sessions (private + normal) and compare
   responses that should be personalized
6. Inspect cf-cache-status on dynamic URLs; BYPASS or DYNAMIC is expected,
   HIT on a personalized page is a defect
7. Re-audit the rule list after any plugin that touches checkout, auth,
   or account pages is added or updated

Testing with two genuinely separate sessions — not just two tabs — is a practical way to catch a leaking cache before a monitoring alert or a customer report does. It is also worth pairing this with cache analytics in the Cloudflare dashboard to confirm hit ratios move in the expected direction after each rule change, rather than assuming a rule works because it saved without error.

None of this replaces reading the current Cache Rules documentation for the account's specific plan tier, since expression limits, evaluation order behavior, and header precedence have changed across Cloudflare's product history and may change again. For broader infrastructure hardening patterns that apply the same "verify before trusting the default" discipline, see the write-ups on kuryzhev.cloud.

Related

Top comments (0)