WordPress + Cloudflare Cache Settings That Won’t Break wp-admin

Was this helpful?

The fastest way to lose a morning is to “speed up WordPress” by turning on aggressive caching at the edge, then discovering
your editors can’t log in, previews show old content, and wp-admin loads like it’s wading through syrup. The quickest way
to lose an afternoon is to fix it by purging everything every five minutes until the site “feels OK.”

You can have speed and a sane admin experience. But you need cache rules that respect how WordPress uses cookies, nonces,
and personalized responses. This is the playbook I use in production: what to cache, what to never cache, how to prove it,
and how to diagnose the weird failures that only show up when the CEO is watching a demo.

The few rules that matter

The edge cache is a blunt instrument. WordPress is a subtle little machine built on cookies, user state, and time-limited
nonces that protect forms and actions. If you cache the wrong response, you don’t just serve stale HTML—you serve the wrong
HTML to the wrong person, and WordPress notices. Often loudly.

Rule 1: Never cache authenticated or stateful responses

In practice, “authenticated” means “has WordPress auth cookies” and “stateful” means “cart/checkout/session cookies” or
pages that include nonces. If a request carries cookies that change what the origin would return, you must bypass cache.
You can still cache static assets for logged-in users; just don’t cache HTML.

Rule 2: Cache public HTML only when you can prove it’s public

“Cache Everything” is fine if you pair it with strong bypass rules based on cookies and paths. Without bypass rules it’s
how you end up caching /wp-admin/ responses and serving them to the next person who asks. Yes, this happens. No, it’s not
fun. Your threat model should include “well-meaning caching” as an adversary.

Rule 3: Use a small number of clear rules, then observe

Don’t build a cathedral of 47 edge rules on day one. Start with: (1) bypass admin/login, (2) bypass when cookies indicate
logged-in or session state, (3) cache static assets aggressively, (4) cache public HTML with a reasonable TTL and a purge
plan. Then measure hit rate and origin load.

Rule 4: If you can’t explain a cache decision, you can’t operate it

Every cache behavior should map to an observable signal: a header (CF-Cache-Status), a log line, a cookie, or a path match.
If the only way to “debug” is to click refresh and hope, you’re not running a cache. You’re running a slot machine.

Joke #1: Caching is easy until it isn’t—kind of like DNS, but with more opportunities to make your own site gaslight you.

Interesting facts and short history

  • Fact 1: WordPress nonces aren’t true cryptographic nonces; they’re time-based tokens designed for CSRF protection with a validity window.
  • Fact 2: The wordpress_logged_in_ cookie is the simplest reliable signal that a request should not be served from public cache.
  • Fact 3: Many WordPress plugins add their own cookies (A/B tests, consent banners, analytics). Some of these are harmless; others change page output.
  • Fact 4: “Cache Everything” on Cloudflare originally became popular as a workaround to cache HTML for sites without a reverse proxy like Varnish.
  • Fact 5: WordPress’s DONOTCACHEPAGE constant exists because even server-side caches can’t safely guess when a page is personalized.
  • Fact 6: Cloudflare’s edge respects certain cache headers, but “respect” and “do exactly what you think” are not the same thing unless you test.
  • Fact 7: A surprising number of “wp-admin is slow” incidents are actually origin CPU contention from PHP workers, not network latency.
  • Fact 8: Purging is operationally expensive. Even when it’s “instant,” it can collapse your hit ratio and stampede your origin.
  • Fact 9: WooCommerce’s cart fragments (historically via ?wc-ajax=get_refreshed_fragments) are a recurring source of cache confusion and partial personalization.

What “won’t break wp-admin” actually means

The bar isn’t “wp-admin loads.” The bar is: login works, nonce-protected actions work, redirects don’t loop, previews show
current content, media uploads succeed, editors don’t see other users’ sessions, and your cache doesn’t leak private data.
That’s the minimum viable safety.

Here’s what commonly breaks when caching rules are sloppy:

  • Login redirect loops: /wp-login.php posts, sets cookies, then edge serves a cached response that ignores the new cookie state.
  • Stale admin UI: caching of /wp-admin/ or admin-ajax responses. One cached “You are not allowed” can ruin an hour.
  • Preview and editor issues: preview links include nonce-like parameters; caching those is a reliable way to show yesterday’s draft as “current.”
  • Personalized toolbar leaks: cache public pages but ignore that logged-in users get the admin bar injected.
  • WooCommerce cart weirdness: cached cart/checkout or fragment endpoints results in phantom carts and support tickets.

If your cache configuration can’t express “cache this only when no state cookies are present,” you’re not ready to cache HTML.
Fortunately, Cloudflare can express that—you just have to do it deliberately.

Baseline Cloudflare settings for WordPress (safe defaults)

The clean baseline is: cache static assets hard, don’t cache admin or login, and only cache public HTML if you have cookie
bypass rules. Everything else is garnish.

1) Cache static assets aggressively

These are safe to cache at the edge even for logged-in users because they don’t vary by user (unless you’re doing something
very creative with CSS, which is a separate discussion and possibly a cry for help).

  • Images: .png .jpg .jpeg .gif .webp .svg
  • CSS/JS: .css .js
  • Fonts: .woff .woff2 .ttf .otf

Use long browser TTLs with versioned filenames. WordPress core and many themes already add query strings; better is hashed
filenames, but don’t let perfect block progress. If you can’t fingerprint assets, keep browser TTL moderate and rely on edge.

2) Bypass cache for admin and login paths

This is non-negotiable:

  • /wp-admin/*
  • /wp-login.php
  • /wp-json/* (at least for authenticated contexts; public endpoints can be cached case-by-case)
  • /xmlrpc.php (ideally block if unused; if used, do not cache)
  • /wp-admin/admin-ajax.php (do not cache)

3) Bypass cache when certain cookies exist

Cloudflare’s edge can’t “understand” WordPress, but it can match cookies. These are the big ones:

  • wordpress_logged_in_* → user authenticated
  • wordpress_sec_* → auth on SSL admin areas
  • wp-postpass_* → password-protected posts
  • WooCommerce cookies: woocommerce_items_in_cart, woocommerce_cart_hash, wp_woocommerce_session_*
  • Some membership plugins set their own cookies; identify and include them

4) Decide what you’re doing for HTML

You have two sane options:

  1. Conservative: Don’t cache HTML at Cloudflare at all. Cache only static assets. Use an origin page cache (plugin, Nginx FastCGI cache, or a reverse proxy) for HTML. This is the “I want predictable behavior” option.
  2. Edge HTML caching: Cache public HTML at Cloudflare with strict bypass rules. This is the “I want maximal offload and global speed” option.

What I avoid: caching HTML sometimes, but without a clear policy. That’s how you get a site that’s fast for you and broken for everyone else.

Cache strategy: static, HTML, and the dangerous middle

Static assets: boring, correct, and profitable

Static caching is where Cloudflare shines with low risk. If your origin is struggling, start here. You’ll cut bandwidth,
reduce TLS handshakes at your box, and get smoother performance during traffic spikes.

HTML: worth it, but only with guardrails

Edge caching HTML can turn a $20 VPS into something that survives the front page of the internet—until it serves the wrong
HTML to the wrong user. The only safe way is to ensure the cached variant represents the same response your origin would
return for all users in that cache bucket.

For most WordPress sites, the safe bucket is “no logged-in cookie and no session/cart cookie.” That’s your public cache.
Everything else bypasses.

The dangerous middle: endpoints that look static but aren’t

These are the subtle ones:

  • Search pages: lots of variants, sometimes personalized; caching can work but needs careful keying and short TTLs.
  • Feeds: can be cached, but publishers hate stale feeds; keep TTL short.
  • REST API: public endpoints can be cached, authenticated endpoints must not.
  • Localized or A/B content: if content varies by cookie or header, your cache key must include it or you must bypass.

If you’re not sure, bypass. Fast and wrong is still wrong, just with better ping times.

Never-cache rules for wp-admin, login, and previews

Paths that should always bypass cache

If you implement only one thing, implement this list. You can translate it to Cloudflare Cache Rules (preferred), older Page
Rules (legacy), or a Worker.

  • *example.com/wp-admin/*Bypass cache
  • *example.com/wp-login.php*Bypass cache
  • *example.com/wp-admin/admin-ajax.php*Bypass cache
  • *example.com/wp-cron.php*Bypass cache (or rate-limit; don’t cache)
  • *example.com/?preview=true*Bypass cache
  • *example.com/*preview_id=* and *preview_nonce=*Bypass cache

Disable performance “features” for admin paths

Some optimizations are great for anonymous traffic and annoying for authenticated users. For admin paths and login paths,
I like the boring choice:

  • Disable HTML minification if it has ever broken your admin UI (it happens with odd plugins).
  • Be cautious with Rocket Loader-style JS rewriting. The admin loads plenty of scripts; you don’t need edge magic there.
  • Keep security features (WAF) enabled, but test that they don’t challenge admins in the middle of actions.

Cookie-aware caching: your real control plane

Path rules keep wp-admin safe. Cookie rules keep everything else safe. Most of the “Cloudflare broke my WordPress” posts
are really “we cached HTML while ignoring cookies that change content.”

Cookie signals you should bypass on

At minimum:

  • wordpress_logged_in_*
  • wordpress_sec_*
  • wp-postpass_*

Add if present:

  • Membership or LMS cookies (plugin-specific)
  • Language selector cookies if your site varies content by cookie
  • A/B testing cookies if they change HTML

What about “comment author” cookies?

WordPress sets cookies like comment_author_* after someone comments. They often don’t materially change page
output for most sites. If you cache HTML and see odd behavior for recent commenters, bypass on these cookies too. If you’re
chasing hit ratio, test first; don’t cargo-cult.

Cache key discipline: don’t vary on every cookie

Varying cache by every cookie destroys hit ratio and can amplify cache storage use. The trick is to bypass when stateful
cookies are present, not to create a million cache variants.

WooCommerce and other personalized pages

WooCommerce adds customer sessions, cart state, and checkout flows that are allergic to incorrect caching. You can still
cache product category pages and product detail pages for anonymous users—often with huge gains—but you must carve out the
transactional paths.

Always bypass cache for these WooCommerce paths

  • /cart/*
  • /checkout/*
  • /my-account/*
  • /?wc-ajax=*
  • Any “add-to-cart” endpoint or query parameter your theme uses

Bypass on these WooCommerce cookies

  • woocommerce_items_in_cart
  • woocommerce_cart_hash
  • wp_woocommerce_session_*

What to cache safely in WooCommerce

Public product pages, category listings, static images, CSS/JS. Keep TTL reasonable (minutes to an hour) unless you have
strong purge integration when inventory/prices change.

Joke #2: If you cache the checkout page, you haven’t built an ecommerce site—you’ve built an interactive guessing game.

Origin headers: Cache-Control, Vary, and why you should care

Cloudflare caching policy is a conversation between your origin, Cloudflare settings, and the request. If you only control
one side of that conversation, you’re going to misunderstand the outcome.

Cache-Control: your origin’s intent

For admin and personalized content, you want:

  • Cache-Control: private, no-store or no-cache depending on your stack

For public HTML you’re willing to cache:

  • Cache-Control: public, max-age=0, s-maxage=... (if you want edge to cache longer than browsers)
  • Or let Cloudflare set an Edge TTL and keep browser TTL modest

Vary: the silent cache key multiplier

Vary tells caches which request headers affect the response. If your origin sends Vary: Cookie,
you effectively make every cookie combination a different object. That can be correct for personalized responses, and
catastrophic for performance if it leaks into public pages.

Prefer avoiding Vary: Cookie on public HTML by bypassing based on cookies instead. If a plugin forces it,
you may need to disable that behavior or accept a lower hit ratio.

One quote you can actually run a system on

Werner Vogels (paraphrased idea): “Everything fails, all the time—design so systems keep working anyway.” That’s caching too:
assume partial failures and stale objects will happen, then contain the blast radius.

Practical tasks: commands, outputs, decisions (12+)

These are the checks I run when someone says “Cloudflare caching broke wp-admin” or “we enabled caching and now things are weird.”
Each task includes a command, sample output, what it means, and what decision you make from it.

Task 1: Confirm whether Cloudflare is caching a public page

cr0x@server:~$ curl -sI https://example.com/ | egrep -i 'cf-cache-status|cache-control|age|server'
server: cloudflare
cf-cache-status: HIT
age: 842
cache-control: max-age=0, s-maxage=3600

Meaning: The response is served by Cloudflare (server: cloudflare) and is a cache HIT with an object age of 842 seconds.

Decision: Public HTML caching is active. Next, verify cookie bypass works for logged-in sessions.

Task 2: Verify wp-admin is not cached (it should be BYPASS or DYNAMIC)

cr0x@server:~$ curl -sI https://example.com/wp-admin/ | egrep -i 'cf-cache-status|location|set-cookie|cache-control'
cf-cache-status: DYNAMIC
cache-control: no-store, no-cache, must-revalidate, max-age=0

Meaning: Cloudflare did not serve this from cache. The origin is signaling no-store/no-cache.

Decision: Path bypass is working for /wp-admin/. If admins still have issues, look at cookies, WAF challenges, or origin performance.

Task 3: Verify wp-login.php is not cached

cr0x@server:~$ curl -sI https://example.com/wp-login.php | egrep -i 'cf-cache-status|cache-control|set-cookie'
cf-cache-status: DYNAMIC
cache-control: no-store, no-cache, must-revalidate, max-age=0

Meaning: Login is not cached. Good.

Decision: If login loops still happen, you likely have a cookie/redirect issue or mixed HTTP/HTTPS settings at WordPress/Cloudflare.

Task 4: Simulate a logged-in cookie and ensure public pages bypass

cr0x@server:~$ curl -sI https://example.com/ -H 'Cookie: wordpress_logged_in_abc=1' | egrep -i 'cf-cache-status|cache-control|age'
cf-cache-status: BYPASS
cache-control: private, no-cache, no-store, must-revalidate

Meaning: Your cookie bypass rule is effective; Cloudflare is not caching HTML when the logged-in cookie exists.

Decision: Safe for authenticated users. If you see HIT here, stop and fix rules before you leak personalized content.

Task 5: Check REST API caching behavior (public endpoint)

cr0x@server:~$ curl -sI https://example.com/wp-json/ | egrep -i 'cf-cache-status|cache-control|content-type'
content-type: application/json; charset=UTF-8
cf-cache-status: DYNAMIC
cache-control: no-cache, must-revalidate, max-age=0

Meaning: REST root is dynamic and not cached.

Decision: Keep it dynamic unless you have a specific reason to cache certain public endpoints with a short TTL and a clear invalidation strategy.

Task 6: Detect accidental caching of previews (classic failure)

cr0x@server:~$ curl -sI "https://example.com/?p=123&preview=true" | egrep -i 'cf-cache-status|cache-control|age'
cf-cache-status: DYNAMIC
cache-control: private, no-cache, no-store, must-revalidate

Meaning: Preview responses are not cached.

Decision: If this is HIT, add bypass rules for preview query parameters immediately.

Task 7: Confirm admin-ajax is not cached

cr0x@server:~$ curl -sI https://example.com/wp-admin/admin-ajax.php | egrep -i 'cf-cache-status|cache-control'
cf-cache-status: DYNAMIC
cache-control: no-store, no-cache, must-revalidate, max-age=0

Meaning: Good. Caching admin-ajax breaks random parts of the admin UI and front-end features.

Decision: If not dynamic, bypass on that path and consider adding a WAF rule rather than caching it.

Task 8: Inspect WordPress cookies from the login page

cr0x@server:~$ curl -sI https://example.com/wp-login.php | egrep -i 'set-cookie|location'
set-cookie: wordpress_test_cookie=WP%20Cookie%20check; path=/; secure; HttpOnly; SameSite=Lax

Meaning: WordPress is setting a test cookie; login flow relies on cookie acceptance.

Decision: If cookies are missing or altered, check Cloudflare Transform Rules/Workers, security headers, or a plugin meddling with cookies.

Task 9: Verify HTTPS correctness and redirect chain

cr0x@server:~$ curl -sI http://example.com/wp-admin/ | egrep -i 'HTTP/|location|server'
HTTP/1.1 301 Moved Permanently
server: cloudflare
location: https://example.com/wp-admin/

Meaning: HTTP correctly redirects to HTTPS.

Decision: If you see loops (http→https→http), fix SSL mode (usually Full/Strict) and ensure WordPress “Site URL” and “Home” are HTTPS.

Task 10: Check whether Cloudflare is serving cached HTML to a request with WooCommerce cookies

cr0x@server:~$ curl -sI https://example.com/product/widget/ -H 'Cookie: wp_woocommerce_session_123=1; woocommerce_items_in_cart=1' | egrep -i 'cf-cache-status|age'
cf-cache-status: BYPASS

Meaning: Correct behavior: don’t cache HTML with WooCommerce session/cart state.

Decision: If you see HIT, add cookie bypass rules and bypass transactional paths immediately.

Task 11: Validate response headers from origin directly (bypassing Cloudflare)

cr0x@server:~$ curl -sI https://origin.example.internal/ | egrep -i 'cache-control|vary|set-cookie|server'
server: nginx
cache-control: public, max-age=0, s-maxage=600
vary: Accept-Encoding

Meaning: The origin intends edge caching for 600 seconds and does not vary on Cookie. That’s good for public pages.

Decision: Align Cloudflare Edge TTL with origin intent, or override carefully. If you see Vary: Cookie, expect hit ratio pain.

Task 12: Check for surprising cookies on anonymous requests

cr0x@server:~$ curl -sI https://example.com/ | egrep -i 'set-cookie'
set-cookie: pll_language=en; path=/; secure; SameSite=Lax

Meaning: A language plugin sets a cookie even for anonymous users. That might or might not change HTML.

Decision: If content varies by that cookie, either vary cache by it (carefully) or bypass for those users; otherwise ignore it to keep hit ratio high.

Task 13: Confirm that a purge actually changes the object (spot stale content)

cr0x@server:~$ curl -sI https://example.com/ | egrep -i 'cf-cache-status|age|etag|last-modified'
cf-cache-status: HIT
age: 3599
etag: "a1b2c3"

Meaning: The edge object is almost at an hour old.

Decision: If you just updated content and expected it to change, you need either (a) better purge triggers, (b) shorter TTL for HTML, or (c) cache-busting via surrogate keys/tags (if your setup supports it).

Task 14: Check origin health and latency when cache is bypassed

cr0x@server:~$ curl -s -o /dev/null -w 'ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/wp-admin/
ttfb=1.842 total=1.913

Meaning: Time-to-first-byte is ~1.8s for admin. That’s origin/PHP/DB time, not Cloudflare.

Decision: Stop obsessing over edge cache and profile PHP workers, database queries, and object cache. Admin speed is mostly on the origin.

Task 15: Inspect PHP-FPM pressure (common wp-admin slowness)

cr0x@server:~$ sudo ss -s
Total: 256 (kernel 0)
TCP:   128 (estab 22, closed 84, orphaned 0, synrecv 0, timewait 84/0), ports 0

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  6         4         2
TCP	  44        28        16
INET	  50        32        18
FRAG	  0         0         0

Meaning: This doesn’t directly show PHP-FPM, but it does show connection churn. Pair it with your PHP-FPM status or process counts.

Decision: If admin is slow during high traffic and cache bypassed paths pile up, increase PHP-FPM capacity or reduce expensive admin plugins rather than tweaking edge TTL.

Task 16: Check Nginx access logs for cache-bypass endpoints getting hammered

cr0x@server:~$ sudo tail -n 5 /var/log/nginx/access.log
203.0.113.10 - - [27/Dec/2025:10:14:01 +0000] "POST /wp-login.php HTTP/2.0" 200 4578 "-" "Mozilla/5.0"
203.0.113.11 - - [27/Dec/2025:10:14:02 +0000] "GET /wp-admin/admin-ajax.php?action=heartbeat HTTP/2.0" 200 321 "-" "Mozilla/5.0"
203.0.113.12 - - [27/Dec/2025:10:14:02 +0000] "GET /wp-admin/admin-ajax.php?action=heartbeat HTTP/2.0" 200 321 "-" "Mozilla/5.0"
203.0.113.13 - - [27/Dec/2025:10:14:03 +0000] "GET /wp-admin/admin-ajax.php?action=heartbeat HTTP/2.0" 200 321 "-" "Mozilla/5.0"
203.0.113.14 - - [27/Dec/2025:10:14:03 +0000] "GET /wp-admin/admin-ajax.php?action=heartbeat HTTP/2.0" 200 321 "-" "Mozilla/5.0"

Meaning: Heartbeat calls can stack up with many editors and inflate PHP load. Caching won’t help because it shouldn’t be cached.

Decision: Tune Heartbeat frequency, reduce concurrent editors, or scale PHP workers. Consider rate-limiting abusive patterns at the edge.

Fast diagnosis playbook

When you’re on the hook and someone is shouting “Cloudflare did it,” you need a short path to truth. This is my
check-first/second/third flow.

First: Is the bad response cached?

  • Check CF-Cache-Status on the failing URL.
  • If it’s HIT or STALE on an admin/login/preview URL, you already found the problem: your bypass rules are wrong.
  • If it’s DYNAMIC or BYPASS, Cloudflare caching likely isn’t the direct cause. Move on.

Second: Is the request stateful (cookies) and you forgot to bypass?

  • Repeat the request with a simulated wordpress_logged_in_* cookie and ensure it bypasses.
  • For WooCommerce, test with cart/session cookies.
  • If cookie-bearing requests are cached, stop. Fix cookie bypass and purge affected HTML.

Third: Is it a redirect/SSL mode mismatch?

  • Check redirect chain from HTTP to HTTPS and from www to apex (or vice versa).
  • Confirm WordPress “Home URL” and “Site URL” match what Cloudflare serves.
  • Cloudflare SSL mode should be Full (Strict) if your origin has a valid cert.

Fourth: Is it actually origin performance?

  • Measure TTFB for /wp-admin/ and other bypassed endpoints.
  • If TTFB is high, optimize PHP/DB/object cache. Admin will never be fast if the origin is melting.

Fifth: Is it a security feature interfering (WAF / bot fight / challenges)?

  • Look for 403/429/JS challenges during admin actions.
  • Allowlist office IPs or enforce SSO/VPN for admin, rather than disabling protection globally.

Common mistakes: symptom → root cause → fix

1) Symptom: wp-admin shows a cached “Please log in” even after login

Root cause: /wp-admin/ or /wp-login.php got cached via “Cache Everything” without bypass.

Fix: Add explicit bypass rules for /wp-admin/*, /wp-login.php, and purge cache. Verify with CF-Cache-Status: DYNAMIC.

2) Symptom: Login redirect loop (wp-login.php → wp-admin → wp-login.php)

Root cause: Mixed scheme (HTTP/HTTPS) or incorrect SSL mode; cookies set for HTTPS but requests bounce via HTTP, or origin thinks it’s HTTP behind proxy.

Fix: Ensure Cloudflare is Full (Strict) to origin, force HTTPS, set WordPress URLs to HTTPS, and ensure the origin sees correct X-Forwarded-Proto / CF-Visitor.

3) Symptom: Preview shows old content or someone else’s draft

Root cause: Preview URLs cached at the edge. Preview parameters aren’t excluded.

Fix: Bypass cache for preview=true, preview_id, preview_nonce, and consider bypassing for logged-in cookies entirely.

4) Symptom: WooCommerce cart displays wrong items or empties randomly

Root cause: Cart/checkout pages cached or fragment endpoints cached. Or cookie bypass missing.

Fix: Bypass cache for cart/checkout/my-account paths, bypass on WooCommerce session/cart cookies, and don’t cache ?wc-ajax=*.

5) Symptom: Logged-in users see the admin toolbar missing or half-broken

Root cause: HTML cached for logged-in users, or edge optimization rewriting JS/CSS in wp-admin/front-end for authenticated sessions.

Fix: Bypass HTML cache for wordpress_logged_in_*. Disable risky script optimizations on admin/login paths.

6) Symptom: Random 403s in wp-admin, especially on save/publish

Root cause: WAF rule matches admin POSTs, or Bot protections challenging authenticated actions.

Fix: Create targeted WAF exceptions for authenticated admin endpoints, keep protection for anonymous traffic, and validate no caching of challenged pages.

7) Symptom: Site is fast for anonymous users, but wp-admin is painfully slow

Root cause: That’s normal if the origin is overloaded. Admin cannot be cached safely, so it exposes PHP/DB slowness.

Fix: Add object caching, tune PHP-FPM, reduce heavy admin plugins, optimize DB, and consider scaling origin. Don’t try to “cache wp-admin.”

8) Symptom: Purge “fixes it” but only for a few minutes

Root cause: You’re masking a wrong cache policy; content is being cached when it shouldn’t, or TTL is too long without purge integration.

Fix: Fix bypass rules, correct TTLs, and implement targeted purges on publish/update. Purging as a coping mechanism is not a strategy.

Three corporate-world mini-stories

Story 1: An incident caused by a wrong assumption

A mid-size company ran a WordPress marketing site behind Cloudflare. A well-meaning engineer saw high origin CPU during a
campaign and enabled “Cache Everything” for the whole zone, figuring WordPress would “know” not to cache admin pages.
They assumed the origin’s Cache-Control headers would protect them everywhere.

For an hour, everything looked great—TTFB dropped, origin load flattened, dashboards got high-fived. Then editors reported
they were “randomly logged out.” A few minutes later, someone posted in the incident channel that wp-admin sometimes loaded
but showed another person’s screen state. Not content leakage, thankfully, but enough to trigger a panic audit.

The root cause was mundane: a single broad rule applied to /* without explicit bypass for /wp-admin/
and /wp-login.php. Some admin responses had cacheable status codes and were cached long enough to create a
carousel of confusion. The origin did send “no-store” most of the time, but not consistently for every edge case and
redirect response in the login flow.

The fix was also mundane: explicit bypass rules for admin/login plus cookie-based bypass for logged-in sessions. Then a full
purge of HTML. The longer-term fix was cultural: no caching change without a quick curl-based validation of CF-Cache-Status
for admin, login, preview, and a cookie-bearing request.

Story 2: An optimization that backfired

A larger org wanted “global performance parity” and decided to cache public HTML at the edge with a long TTL to reduce
origin costs. They wired up scheduled purges every hour “just to be safe.” It sounded reasonable in a spreadsheet.

In production, the hourly purge created a predictable origin stampede. Right after the purge, the hit ratio collapsed,
every page was a MISS, and the origin got hammered. The load balancer started queueing, PHP workers saturated, and wp-admin
became unusable for editors precisely during the periods when content teams were most active.

To make it worse, a few pages were personalized by a consent banner plugin that injected slightly different HTML based on a
cookie. The cache key wasn’t varying by that cookie (because varying by cookies would have destroyed hit ratio), so after
each purge, whichever variant got cached first won the lottery for everyone else until it expired.

The eventual solution wasn’t flashy. They shortened TTL for HTML to something that matched content velocity, removed the
scheduled purge, and implemented targeted purges on publish/update. For the consent plugin, they adjusted it to avoid
changing the core HTML for anonymous users and moved personalization into client-side behavior.

The lesson: if your cache requires frequent full purges to stay correct, the cache policy is wrong. You don’t need a bigger
purge button. You need fewer reasons to press it.

Story 3: The boring but correct practice that saved the day

Another team had a strict “change with proof” policy. Every Cloudflare caching change required a tiny test script run from
a bastion host: fetch a public page, fetch wp-admin, fetch wp-login, fetch a preview URL, fetch the homepage with a simulated
logged-in cookie, and record the key headers.

It felt slow, especially when product teams wanted instant performance wins. But the team had scars. They’d seen caches
leak state before, and they preferred boring over dramatic.

One Friday, a new optimization was proposed: edge caching HTML for a set of marketing landing pages. The engineer ran the
script and noticed something odd: the landing pages set a cookie from a marketing automation tool on first view, and the HTML
subtly changed to include a personalized tracking pixel state.

Because the cookie was present for many users, the cache would have served the “cookie-present” variant to cookie-absent users
and vice versa, and the marketing team would have accused analytics of being “flaky.” Instead, they implemented edge caching
only for the assets and kept HTML cache conservative until they could refactor the landing page scripts.

Nothing exploded. Nobody got paged. The weekend stayed intact. This is what success often looks like in operations: quiet.

Checklists / step-by-step plan

Step-by-step: safe Cloudflare cache configuration for WordPress

  1. Start with static assets only.

    • Set long Edge TTL and Browser TTL for versioned assets.
    • Validate CF-Cache-Status: HIT for .css, .js, images.
  2. Install non-negotiable bypass rules.

    • Bypass /wp-admin/*, /wp-login.php, /wp-admin/admin-ajax.php, preview query params.
    • Verify they show CF-Cache-Status: DYNAMIC or BYPASS.
  3. Add cookie-based bypass for authenticated and stateful sessions.

    • Bypass when cookie matches wordpress_logged_in_*, wordpress_sec_*, wp-postpass_*.
    • If ecommerce: bypass WooCommerce cookies and cart/checkout paths.
  4. If caching HTML at the edge, do it explicitly and narrowly.

    • Cache public HTML only when no state cookies are present.
    • Set a sane TTL (start 5–15 minutes) and adjust after observing content update frequency.
  5. Define purge policy.

    • Prefer targeted purges on publish/update events.
    • Avoid scheduled full-site purges unless you enjoy origin stampedes.
  6. Test with real flows.

    • Log in, edit, preview, publish, upload media, update menus.
    • Verify no admin endpoints are cached and no stale preview appears.
  7. Observe and iterate.

    • Track hit ratio, origin TTFB, and error rates.
    • Any new plugin that sets cookies becomes a cache review item.

Operations checklist: before and after any caching change

  • Confirm /wp-admin/ is not cached.
  • Confirm /wp-login.php is not cached.
  • Confirm preview URLs are not cached.
  • Confirm homepage is cached for anonymous requests (if you intend HTML caching).
  • Confirm homepage bypasses when wordpress_logged_in_* cookie is present.
  • Confirm WooCommerce cart/checkout bypass (if applicable).
  • Confirm redirect chain is correct (HTTP→HTTPS, www normalization).
  • Confirm no WAF challenge appears mid-admin action.
  • Record headers before/after: CF-Cache-Status, Cache-Control, Vary.
  • Have a rollback plan: disable HTML caching rule, purge HTML, keep static caching.

FAQ

1) Can I cache wp-admin to make it faster?

No, not safely. wp-admin is authenticated, nonce-heavy, and user-specific. Make it faster by fixing PHP/DB/object cache and
reducing admin plugin overhead. Cache static assets globally; leave admin dynamic.

2) Is “Cache Everything” always bad for WordPress?

Not always. It’s dangerous when used broadly without strict bypass rules. If you pair it with bypass for admin/login/preview
and cookie-based bypass for stateful sessions, it can work well for public content.

3) What’s the minimum cookie bypass list for WordPress?

wordpress_logged_in_*, wordpress_sec_*, and wp-postpass_*. Then add WooCommerce cookies or
membership plugin cookies as needed.

4) Why do previews break specifically?

Preview URLs include parameters that represent a time-limited permission to view draft content. If you cache that response,
you can serve stale drafts—or serve a preview to someone who shouldn’t see it. Bypass preview parameters.

5) Do I need to bypass /wp-json/?

For authenticated requests, yes. For public endpoints, it depends. If you don’t have a clear benefit and a short TTL plan,
keep it dynamic. REST caching is easy to get subtly wrong.

6) My site sets lots of cookies for anonymous users. Does that mean I can’t cache HTML?

Not necessarily. The question is whether the HTML varies because of those cookies. If it does, move personalization
client-side or vary cache carefully on a small, controlled set. If it doesn’t, ignore them for cache decisions.

7) Why is my hit ratio low even though I enabled caching?

Common reasons: you’re varying on too many things (headers/cookies), TTL is too short, query strings create many variants,
or you’re purging too often. Measure CF-Cache-Status on top URLs and reduce unnecessary variation.

8) Should I rely on origin Cache-Control or Cloudflare Edge TTL settings?

Ideally both align. In practice, I prefer explicit Cloudflare rules for what’s cached and for how long, then keep origin
headers sane: “no-store” for admin/personalized, reasonable cache hints for public content. Test the result, don’t debate it.

9) Do I need to purge on every post update?

If you cache HTML at the edge and editors expect near-instant updates, yes—at least for affected pages (post URL, homepage,
category pages). If you can tolerate a short delay, set a shorter TTL and purge less. Choose one: operational simplicity or
instant freshness.

10) What’s the safest first performance win that won’t touch wp-admin?

Cache static assets at the edge with long TTLs, enable compression, and optimize origin PHP/DB for admin. This gives you
user-visible speed without risking authenticated flows.

Conclusion: next steps that stick

Cloudflare caching doesn’t “break wp-admin.” People break wp-admin by caching things that should never be cached, then
assuming the edge will magically infer user state from vibes. The fix is mechanical: carve out admin/login/preview, bypass
on state cookies, and treat HTML caching as a controlled feature with tests.

Practical next steps:

  1. Run the header checks for homepage, wp-admin, wp-login, and a preview URL. Record CF-Cache-Status.
  2. Implement path bypass rules for admin/login/admin-ajax and preview parameters. Verify immediately with curl.
  3. Add cookie-based bypass for WordPress auth cookies and ecommerce/session cookies.
  4. If you cache HTML, start with a short TTL and targeted purges on publish. Never schedule full purges as a lifestyle.
  5. When wp-admin is slow, stop blaming the cache and measure origin TTFB. Then tune PHP-FPM and the database like an adult.
← Previous
WordPress REST API error: what breaks REST and how to troubleshoot
Next →
Proxmox Backup “No space left on device”: why it fails even when space seems free

Leave a comment