The WordPress + Cloudflare cache invalidation order that nobody explains
How to purge 5 cache layers in WordPress (OPcache, Object Cache, WP Rocket, LiteSpeed, Cloudflare) without serving stale content. Exact order.
Summary
- The problem: modern WordPress runs 5 stacked cache layers (OPcache, Object Cache, WP Rocket, LiteSpeed, Cloudflare). Purge them in the wrong order and you keep serving stale content to users and Googlebot.
- The rule: purge inside-out, always. OPcache → Object Cache → WP Rocket → LiteSpeed → Cloudflare → (browser, you no longer control).
- The typical mistake: purging Cloudflare first. Cloudflare fetches from origin, finds stale content from WP Rocket, caches it again. You're back to square one.
- The practical trick: automate the order with a mu-plugin (must-use plugin) that listens to `save_post` and purges in chain.
- The real case: one of our sites served a stale H1 for 4 hours after a change, until we understood this.
This is the story of how a client called us in a panic on a Saturday because “the homepage H1 was still the old one after four hours and after purging the cache”. In a rush, one cache layer underneath kept serving the old version. After that day we wrote the note we’ve been applying for years.
Modern WordPress runs 5 stacked cache layers. Purge them in the wrong order and a “higher” layer re-caches stale content from a “lower” layer you haven’t cleaned yet. Counterintuitive. And it breaks deploys every week.
The 5 layers, closest to farthest from origin
Picture an onion. The origin (your PHP + MySQL) is inside. The user request comes from outside and, before reaching origin, crosses every layer. If it finds a cached version along the way, the response is served from there without touching the origin.
Layer 5 (innermost) — OPcache + Object Cache
OPcache is precompiled PHP bytecode. When a visitor loads a page and PHP needs to run, OPcache skips re-interpreting source code from scratch. Speeds up any PHP execution 2× to 5×.
Object Cache (usually Redis or Memcached) caches database queries. Every WP_Query, get_post(), get_option() you’ve already executed sits in RAM. Typical drop: 50-70% fewer MySQL queries.
Caches live in server RAM. They don’t know about the outgoing HTML. They’re the foundation.
Layer 4 — WP Rocket (or equivalent plugin)
WP Rocket generates precompiled static HTML for every page on your WordPress. When a request arrives, instead of running PHP, it serves a ready-made HTML file from disk (wp-content/cache/wp-rocket/).
WordPress-level plugin. Knows about posts, taxonomies, language.
Equivalent alternatives: LiteSpeed Cache plugin, W3 Total Cache, FlyingPress, WP Super Cache.
Layer 3 — LiteSpeed Web Server (LSWS)
If your hosting uses LiteSpeed Web Server as its web server (instead of Apache or Nginx), LSWS has its own cache at server level, not WordPress level.
It’s faster than WP Rocket because LSWS serves the cached response without spinning up PHP-FPM. Only applies if your hosting is LiteSpeed. On pure Nginx + PHP-FPM, this layer doesn’t exist (the equivalent would be Nginx FastCGI cache, a different world).
Layer 2 — Cloudflare (global edge)
Cloudflare caches your HTML across its 200+ POPs (points of presence) spread around the world. When a user in Tokyo asks for your page, they don’t reach your server in Madrid — they hit the Tokyo POP, which has a cached copy.
For WordPress, the reasonable move is enabling Cache Everything (via Page Rule or Cache Rule) on public URLs, excluding /wp-admin/ and /wp-login.php.
Layer 1 (outermost) — User browser
Your browser caches based on the HTTP headers you send:
Cache-Control: public, max-age=31536000, immutable→ caches for 1 year, does not revalidate. For versioned assets (hash-named CSS, JS).Cache-Control: no-cache→ caches but revalidates every time with ETag. For HTML.- No
Cache-Control→ browser default (unpredictable). Bad.
You don’t control the user’s cache directly. But you push revalidation by sending correct ETags and responding 304 when nothing changed.
The purge order: inside-out
Here’s the carved-in-stone rule:
1. OPcache (only if you deploy new code)
2. Object Cache (only if you invalidate query-level state)
3. WP Rocket (plugin static HTML)
4. LiteSpeed (server cache)
5. Cloudflare (global edge)
6. Browser (no longer yours, but headers were already correct)
If you purge Cloudflare first, Cloudflare fetches from origin to repopulate → finds LiteSpeed and WP Rocket still holding the old HTML → caches the old HTML → you burned a purge for nothing and you’re still serving stale.
If you purge Object Cache before WP Rocket, WP Rocket keeps serving old HTML generated from queries that have already changed. Another wasted purge.
Real case: how it broke for us
Apparently innocent change: we updated the homepage H1 on a client site. Client publishes, sees the changes from the editor (which bypasses cache), all looks good.
Next day: users and external tools still see the old H1. Support call: “the new version isn’t loading”.
What happened:
- WP Rocket held the old HTML in
wp-content/cache/wp-rocket/. The plugin has a hook that purges onsave_post, but someone had excluded the homepage from auto-purge long ago. - LiteSpeed held the old version in RAM. Its auto-purge depends on WP Rocket or the
litespeed-cacheplugin signalling it — the signal never arrived. - Cloudflare held the old version at the edge. Edge cache TTL = 1 hour, but the client had gotten used to the edge never failing.
We fixed it manually:
1. wp cache flush (Object Cache → Redis)
2. wp rocket clean (WP Rocket → static HTML)
3. LiteSpeed Cache → Purge ALL (hosting panel)
4. Cloudflare → Purge Everything (CF panel)
Four purges back-to-back in the right order. Three minutes later, the new H1 was visible globally.
The automation: a mu-plugin listening to events
Instead of repeating this manually every time the client edits a post, we wrote a mu-plugin (must-use plugin: a PHP file WordPress always loads, without appearing on the activable plugin list). Lives in wp-content/mu-plugins/cache-purge-chain.php.
What it does, simplified:
<?php
add_action('save_post', function ($post_id) {
// 1. Object Cache
wp_cache_flush();
// 2. WP Rocket
if (function_exists('rocket_clean_post')) {
rocket_clean_post($post_id);
}
// 3. LiteSpeed Cache
if (defined('LSCWP_V')) {
do_action('litespeed_purge_post', $post_id);
}
// 4. Cloudflare (via REST API)
purge_cloudflare_url(get_permalink($post_id));
}, 99, 1);
The purge_cloudflare_url() function calls Cloudflare’s API with an API token scoped to purge cache for that zone. Important: do NOT purge all of Cloudflare on every save_post — that costs one purge from the free quota (1,000/day). Purge only the affected URL.
An important warning: a mu-plugin must NOT contain presentation JS or CSS. We once dropped layout code inside one and crashed a whole site. Mu-plugins are for server-side logic only.
Global vs selective purge
| Change | Purge |
|---|---|
| Edit of a single post / page | Selective (only that URL) |
| New menu or sidebar widget | Selective (affected URLs) |
| Theme change | Global (everything) |
| New plugin affecting many pages | Global |
| WordPress core update | Global |
| PHP code deploy | Global + OPcache reset |
| Critical CSS/JS change | Global + bump version querystring |
Global purge is expensive: after one, the first hit on every URL forces a full pipeline recompile (PHP → HTML → cache). If your site has 10,000 URLs, the first 10,000 hits go slow. Reserve for structural changes.
What you don’t see: proper TTLs
The strategy complementary to purging is TTL (Time To Live: how long a cache entry lives before it expires on its own, without a purge).
My recommendations for WordPress:
- OPcache:
opcache.revalidate_freq=60(revalidates every 60s in production, 0 in dev). - Object Cache: default TTL 1 hour, critical fragments 5 min.
- WP Rocket: TTL 10 hours (covers an active user’s session).
- LiteSpeed: TTL 1 day (manual purge on change).
- Cloudflare: Edge Cache TTL 4 hours for HTML, 1 year for versioned assets (hash-named CSS, JS, images).
- Browser Cache:
Cache-Control: no-cachefor HTML (always revalidates with ETag),max-age=31536000, immutablefor versioned assets.
With those TTLs + auto-purge on save_post, you cover 99% of cases without ever opening a panel to purge manually.
What SeoNova does for you
This entire cache-layer dance is built into the WPO Toolkit we’re packaging into SeoNova: install the plugin, connect your Cloudflare + LiteSpeed + WP Rocket credentials, and the system purges in the correct order every time you edit something. No mu-plugin to write, no API tokens to juggle, no mental order to remember.
If that sounds useful, join the waitlist for 50% off the first 3 months. Launching autumn 2026.
Frequently asked questions
The questions we hear the most about this topic
Why so many cache layers if one should suffice?
Isn't purging Cloudflare enough?
What about the user's browser cache?
What if I only change a footer widget on a single page? Purge everything?
Do I have to purge OPcache too?
Keep reading
More posts you might like
- WordPress Security
Free Cloudflare: how to set it up and 5 WAF anti-bot rules for your WordPress
Why free Cloudflare is brutal, how to set it up step by step, and 5 WAF rules that block 60-70% of malicious bots without paying a cent.
9 min read - WordPress Security
WordPress Application Passwords: pros, risks, and a step-by-step guide to create one
What WordPress Application Passwords are, when to use them, risks, and a step-by-step guide to create and revoke one without your main password.
7 min read - Technical SEO
Why Google does not index you: bots are eating your crawl budget (and how to fix it)
Your hosting is saturated with malicious bots eating your crawl budget. That is why Googlebot reduces its visit frequency. Real data and a 30-min fix.
8 min read