
A Cloudflare account does not make a WordPress site faster. The default configuration leaves HTML uncached, which is the part WordPress is slowest at serving. Meanwhile half the switches recommended in older tutorials either no longer exist or actively break checkout.
Start with one command. It answers whether Cloudflare is in the request path at all, and what it currently thinks of your homepage.
SITE="${SITE:?export SITE to your bare hostname first}"
curl -sI "https://$SITE/" | tr -d '\r' | grep -iE '^(server|cf-ray|cf-cache-status|cf-apo-via|cf-edge-cache):'
cf-ray means traffic is proxied. If the output shows server: hcdn, server: LiteSpeed, or server: nginx with no cf-* header, you are talking to the origin or to a different CDN. Nothing else in this article applies until the proxy is live.
That is not hypothetical. I ran the check against the site I publish on while drafting this, and it returned server: hcdn with an x-hcdn-cache-status header — Hostinger’s edge, not Cloudflare’s. So the commands below use a SITE variable. Pasting headers I did not observe would be worse than useless.
The rest is an order of operations. TLS first, because the wrong SSL mode produces failures that look like caching bugs. Caching second, because it is the only feature that can serve one visitor’s page to another. JavaScript third. Everything else is preference.
Prove the Proxy Is in Front of the Site
Two headers tell you almost everything:
cf-rayends in the airport code of the edge that answered. Its presence means the request went through Cloudflare.cf-cache-statusreports the cache outcome:HIT,MISS,DYNAMIC,EXPIRED,BYPASS, orREVALIDATED.DYNAMICis the one that matters most, because it means Cloudflare chose not to cache, and the reason is usually a setting you control.
If cf-ray is missing, check the DNS record. In Cloudflare’s DNS tab, the record must be Proxied (orange cloud). Per Cloudflare’s documentation on proxy status, only records used for IP address resolution — A, AAAA, and CNAME — can be proxied. MX and TXT records are always DNS-only. A common misconfiguration is pointing the www CNAME at a hostname that is itself grey-clouded, which quietly removes the edge.
Set SSL/TLS to Full or Full (Strict) First
Cloudflare terminates TLS at the edge and opens a second connection to your origin. The encryption mode decides what happens on that second hop.
Flexible encrypts the visitor-to-Cloudflare leg and sends plain HTTP from Cloudflare to your server. Three things follow:
- The traffic between Cloudflare and your host is unencrypted, so the padlock is describing a connection that stops halfway.
- If WordPress forces HTTPS — and it does once
siteurlstarts withhttps://, or once a redirect plugin runs — the origin answers the plain-HTTP request with a redirect to HTTPS, Cloudflare fetches it over HTTP again, and you getERR_TOO_MANY_REDIRECTS. - Assets that WordPress emits as
https://can be blocked as mixed content if the origin serves them overhttp://.
Full encrypts both hops but does not validate the origin certificate. Full (strict) validates it, which is the mode you want. Install a Cloudflare Origin CA certificate on the server, or use the Let’s Encrypt certificate already there, then switch.
Verify after the change:
curl -s -o /dev/null -w 'http %{http_code} -> %{redirect_url}\n' "http://$SITE/"
curl -sI "https://$SITE/" | tr -d '\r' | grep -iE '^(strict-transport-security|location):'
The first line should report a 301 or 308 pointing at the https:// version, and it should take exactly one hop. If the redirect target is another http:// URL, the loop is still there. Only add HSTS once the HTTPS version loads cleanly, because browsers cache the header and you cannot easily undo it.
If you are already stuck in a loop, the too many redirects fix walks through the origin side, and the SSL certificate guide covers which certificate type to install.
The Default Does Not Cache HTML
Cloudflare caches by file extension out of the box. Images, CSS, JS, fonts, and a short list of other static types get cached. An HTML page — the WordPress post, the shop archive, the homepage — is returned as DYNAMIC and goes to your PHP process every time.
That is why a site can show cf-ray and still serve its homepage from PHP on every single request. The edge is doing nothing for the request that matters most.
There are two ways to change it, and they are not equivalent.
APO Caches HTML for Anonymous Visitors
Automatic Platform Optimization is Cloudflare’s WordPress-specific product. Cloudflare’s APO documentation describes it as caching dynamic HTML at the edge, which removes the round trip to origin for the page itself.
The important part is what it refuses to cache. APO only stores HTML when all of these hold:
- The request is
GETorHEAD. - The request accepts HTML, and the URL is not a static extension.
- The origin returns
200withContent-Type: text/html. - The
cf-edge-cacheresponse header from the official Cloudflare WordPress plugin permits caching. - No bypass cookie is present — logged-in, session, or WooCommerce cookies.
- No cache-bypassing request header is present.
- The path is not excluded. APO skips checkout,
wp-cron.php, and feeds by default. - Query strings are absent or limited to supported marketing parameters.
When a request bypasses, the response carries cf-apo-via with the reason. That header is the fastest way to answer “why is my homepage not cached” without guessing.
APO is a paid add-on and it wants the Cloudflare WordPress plugin installed. The plugin supplies the cf-edge-cache header and purges the edge when you publish or edit, which is the part that keeps APO from serving yesterday’s post. Skip the plugin and you get caching without invalidation.
Cache Everything Breaks the Pages That Set Cookies
The alternative is a Cache Rule with Eligible for cache and an edge TTL, applied broadly. This is the setting that produces the horror stories: a cached /cart/ page, a cached /checkout/ with a stale nonce, a logged-in visitor receiving a page rendered for someone else.
WooCommerce makes this worse because it keeps cart state in a cookie plus a database row. The cookies to exclude are:
wordpress_logged_in_— any authenticated session.comment_author_— a commenter’s stored identity.woocommerce_items_in_cart— set when the cart is non-empty.wp_woocommerce_session_— the session key that ties the visitor to their cart row.woocommerce_cart_hash— changes when cart contents change.
A bypass expression covering paths and cookies:
(http.request.uri.path contains "/wp-admin/")
or (http.request.uri.path contains "/wp-login.php")
or (http.request.uri.path contains "/wp-cron.php")
or (http.request.uri.path contains "/wp-json/")
or (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 "wordpress_logged_in_")
or (http.cookie contains "woocommerce_items_in_cart")
or (http.cookie contains "wp_woocommerce_session_")
or (http.cookie contains "comment_author_")
Set Cache eligibility to Bypass cache for that expression. The field names come from the Cache Rules settings reference, which also lists the operators each field supports.
Then test it two ways. The Rules simulator in the dashboard evaluates a URL against your rule set without waiting for real traffic, which is the only practical way to check a bypass on a path you cannot easily reach. After that, confirm with real requests:
curl -sI "https://$SITE/cart/" | tr -d '\r' | grep -i '^cf-cache-status:'
curl -sI "https://$SITE/wp-login.php" | tr -d '\r' | grep -i '^cf-cache-status:'
curl -sI "https://$SITE/" | tr -d '\r' | grep -i '^cf-cache-status:'
The first two must say DYNAMIC. The third should say HIT on the second request. If the third is also DYNAMIC, either no rule made it eligible or a bypass cookie from your browser is in play — test in a private window.
Pick APO or Cache Everything, Not Both
Running both means two systems decide cacheability, and the loser is whichever one you forgot about. APO’s rules and a broad Cache Rule will disagree about query strings and about the cf-edge-cache header, and the debugging gets ugly fast.
My preference is APO on any site with WooCommerce, membership, or comments, because the bypass list is maintained by someone who knows the plugin’s cookie behavior. Use Cache Everything only on a site that serves identical HTML to every visitor — a brochure site, a docs site, a static blog — and only with the bypass rule above in place.
Rocket Loader: Keep It Only If Nothing Breaks
Rocket Loader defers all JavaScript, inline and external, until after rendering, while preserving execution order. It is available on every plan and it is genuinely effective at moving First Contentful Paint.
It also rewrites script tags, and that is where sites break. Cloudflare’s own limitations section says it plainly: if you observe JavaScript or jQuery issues, disable Rocket Loader and retest. The same section notes that Rocket Loader uses non-standard tags that fail strict HTML validation, and that a Content Security Policy needs updating to allow it.
So test the paths where JavaScript actually earns money:
- Add to cart, then change quantity, then remove an item.
- Apply a coupon and watch for the “session expired” notice.
- Open the mobile menu, then a product gallery, then a slider.
- Submit the contact form and confirm the success message.
- Open the browser console on each of those pages and read the errors.
If anything fails, turn Rocket Loader off, purge, and repeat. A menu that opens without Rocket Loader and fails with it is not a WordPress bug, and no amount of plugin disabling will fix it.
Cookies, Scripts, and the Cache Plugin You Already Have
If a caching plugin is active on the origin — LiteSpeed Cache, WP Rocket, W3 Total Cache — you now have two caches in series. That is fine when both purge. It is a disaster when only one does.
The rule I use: the origin cache owns invalidation, and Cloudflare owns delivery. When you publish, both should hear about it. LiteSpeed and WP Rocket both ship Cloudflare integrations; enable the one you have so a single purge clears both layers. If you are choosing an origin cache, the free cache plugin comparison covers what each one actually stores.
The tell for a split-brain cache is a page that updates for logged-in users and stays stale for everyone else. Logged-in requests bypass Cloudflare, so what you see in the admin is the origin copy. The public sees the edge copy. Purge both and the discrepancy disappears.
At some order volume, the bottleneck moves off the page cache entirely and onto cart sessions and order writes. The WooCommerce scaling guide covers the queue and database side, which no edge cache can fix.
Restore Visitor IPs or Every Log Is Wrong
When Cloudflare proxies a request, your origin sees a Cloudflare IP. Every log, rate limit, and fraud check that keys on the remote address is now looking at the wrong machine.
Cloudflare’s HTTP headers documentation lists CF-Connecting-IP as the header carrying the original visitor address, and it is the one your web server should trust.
On nginx:
# /etc/nginx/conf.d/cloudflare-ips.conf
# Regenerate from https://www.cloudflare.com/ips-v4 and /ips-v6
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 104.16.0.0/13;
# ... add the rest of the published ranges ...
real_ip_header CF-Connecting-IP;
real_ip_recursive on;
On Apache, mod_remoteip with RemoteIPHeader CF-Connecting-IP does the same job. WordPress plugins that log IPs usually read REMOTE_ADDR, so they benefit automatically once the web server rewrites it.
This is not cosmetic. Fail2ban counts login failures by IP; if every request appears to come from one of Cloudflare’s ranges, a single attacker can get your whole edge blocked. WooCommerce’s fraud checks and any country-based rule have the same problem.
Rate Limiting Without Blocking Googlebot
Rate limiting rules are the right tool for login and XML-RPC abuse, and the wrong tool for “protect the whole site” unless the threshold is generous.
A starting rule:
- Expression:
http.request.uri.path contains "/wp-login.php" or http.request.uri.path contains "/xmlrpc.php" - Counting characteristic: IP address
- Rate: 10 requests per minute
- Action: Managed Challenge
- Period: 60 seconds
Ten per minute is generous for a human who mistyped a password and tight enough to slow credential stuffing. Blocking outright is tempting, but a shared office IP will produce a support ticket you cannot explain.
The crawler problem is subtler. Rate limits count hits, and a site with a large sitemap can produce a burst of legitimate crawl requests. On plans with Bot Management, the cf.bot_management.verified_bot field lets you exclude verified crawlers from the expression. Without it, scope the rule to login paths and leave content paths alone.
After enabling anything in the WAF or bot products, open Search Console’s crawl stats a week later and check that the response-code mix has not shifted. A jump in 5xx or 429 responses to Googlebot is the signal that a security rule is now competing with your SEO.
Turnstile Belongs on Forms, Not on Every Page
Turnstile issues a token in the browser and verifies it server-side. It is a good replacement for image CAPTCHAs because it does not require a puzzle from most visitors.
Put it on the form, not on the page rule. A challenge applied to all HTML breaks two things: cached pages that were built without a token, and any client that does not run JavaScript — including some feed readers and monitoring tools. If a form is cached at the edge, the token in the HTML expires while the page sits in cache, and every submission fails validation.
Forms that should carry it: login, registration, contact, comments, and checkout for guest orders. Forms that should not: search, newsletter popovers rendered client-side, and anything inside the admin.
A Ten-Minute Audit
Run these after any change. Each one maps to a specific failure.
SITE="${SITE:?export SITE to your bare hostname first}"
# 1. Is the edge in the path?
curl -sI "https://$SITE/" | tr -d '\r' | grep -i '^cf-ray:'
# 2. Is a normal page cached on the second request?
curl -s -o /dev/null "https://$SITE/"
curl -sI "https://$SITE/" | tr -d '\r' | grep -i '^cf-cache-status:'
# 3. Are the private paths excluded?
for p in cart/ checkout/ my-account/ wp-login.php; do
printf '%-16s ' "$p"
curl -sI "https://$SITE/$p" | tr -d '\r' | grep -i '^cf-cache-status:' || echo 'no header'
done
# 4. Does plain HTTP redirect once, to HTTPS?
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' "http://$SITE/"
# 5. Is HSTS present?
curl -sI "https://$SITE/" | tr -d '\r' | grep -i '^strict-transport-security:'
Two interpretation notes. The /checkout/ and /my-account/ URLs may return 404 on a site without WooCommerce — that is fine, the header still tells you whether the path was excluded. And step 2 only works if the URL you test is actually cacheable; a page that sets no-cache at the origin will stay DYNAMIC no matter what the edge rule says.
What to Leave Off
- Flexible SSL. It exists for hosts that cannot serve HTTPS. If yours can, it is a downgrade.
- Cache Everything as a Page Rule. Cache Rules give you the same control plus status-code TTLs and a simulator.
- Email Obfuscation. It injects JavaScript to reassemble addresses, which breaks copy-paste and adds a render dependency for one link.
- Bot Fight Mode on API-driven sites. If a mobile app, feed reader, or uptime monitor talks to your site, challenge-based bot protection will interrupt it.
- Rocket Loader, by default. Enable it, test the five flows above, and keep it only if all five pass.
Cloudflare is a large product surface pretending to be a small one. The features that matter for WordPress are TLS mode, one cache layer with an honest bypass list, and correct client IPs. Everything past that is tuning, and tuning is only worth doing once the first three are right. If you are still choosing an edge provider, the CDN comparison covers the alternatives, and the security guide covers the origin-side hardening that Cloudflare cannot do for you.
FAQ
Does Cloudflare make WordPress slower?
It can, in two situations: when the origin is already fast and the extra DNS and TLS hop is not repaid, and when TLS mode forces a redirect chain. A single hop to a nearby edge is usually a few milliseconds. A Flexible configuration that redirects twice is worse than no proxy at all.
Do I still need a caching plugin with Cloudflare?
For page HTML, APO or a Cache Rule can replace it. For object caching, database query caching, and CSS/JS optimization, no. Those happen on the origin. The two layers complement each other as long as both purge on publish.
Why is my cart page showing another customer’s items?
Because HTML was cached without excluding the WooCommerce session cookie. Add wp_woocommerce_session_ and woocommerce_items_in_cart to the bypass expression, purge everything once, and confirm /cart/ returns cf-cache-status: DYNAMIC in a private window.
Is Flexible SSL ever the right choice?
Only when the origin genuinely cannot present a certificate — some shared hosts and legacy appliances. If the host supports Let’s Encrypt or a Cloudflare Origin CA certificate, Full (strict) is the correct setting and Flexible is an unnecessary weakening of the connection.
Does Rocket Loader improve Core Web Vitals?
It can move lab metrics by deferring scripts, but INP is about interaction latency, not initial script parsing. On a site with heavy third-party scripts, Rocket Loader often helps LCP and leaves INP unchanged. Measure both before and after on your own pages.
Will Cloudflare block Googlebot?
Not by default. The risk comes from rate limits, bot fight products, and challenges applied too broadly. Exclude verified bots where your plan allows it, and watch Search Console crawl stats for a rise in 429 or 5xx responses after enabling anything in the WAF.
How do I find out which Cloudflare feature broke my site?
Turn off the last thing you enabled, purge, and retest. If the problem disappears, it was that feature. cf-apo-via and cf-cache-status tell you why a page was not cached; the Rules simulator tells you which rule matched. Guessing without those three signals wastes hours.
Should I enable HSTS?
Yes, after HTTPS works on every subdomain you intend to keep. HSTS tells browsers to refuse plain HTTP for a fixed period, and browsers remember it. Start with a short max-age, confirm nothing breaks, then raise it toward a year and consider the preload list.