← Back to blog

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.

By SeoNova · Published · 9 min read
Diagram of 5 stacked cache layers: browser, Cloudflare, LiteSpeed, WP Rocket, OPcache/Object Cache. Numbered purge order on the left, inside-out.
Diagram of 5 stacked cache layers: browser, Cloudflare, LiteSpeed, WP Rocket, OPcache/Object Cache. Numbered purge order on the left, inside-out.

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:

  1. WP Rocket held the old HTML in wp-content/cache/wp-rocket/. The plugin has a hook that purges on save_post, but someone had excluded the homepage from auto-purge long ago.
  2. LiteSpeed held the old version in RAM. Its auto-purge depends on WP Rocket or the litespeed-cache plugin signalling it — the signal never arrived.
  3. 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

ChangePurge
Edit of a single post / pageSelective (only that URL)
New menu or sidebar widgetSelective (affected URLs)
Theme changeGlobal (everything)
New plugin affecting many pagesGlobal
WordPress core updateGlobal
PHP code deployGlobal + OPcache reset
Critical CSS/JS changeGlobal + 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-cache for HTML (always revalidates with ETag), max-age=31536000, immutable for 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?
Because each layer optimises a different thing. OPcache caches compiled PHP (speeds up the language). Object Cache (Redis/Memcached) caches WP queries (speeds up the database). WP Rocket generates static HTML (avoids running PHP). LiteSpeed Web Server caches the response at server level (faster than WP Rocket). Cloudflare caches at the global edge (serves without touching your server). Removing any of the 5 drops your mobile PSI by 8 to 25 points.
Isn't purging Cloudflare enough?
No. When you purge Cloudflare, it fetches from origin (your server) to repopulate. If LiteSpeed and WP Rocket still hold the old HTML, Cloudflare caches the old version again. You've burned a purge for nothing. That's why you have to purge **inside-out**.
What about the user's browser cache?
You don't control it directly. What you do control is the `Cache-Control` HTTP header you send. With `Cache-Control: public, max-age=31536000, immutable` on versioned assets (hash-named CSS, JS) and `Cache-Control: no-cache` on HTML, the browser caches only the right things. HTML always revalidates with ETag/If-None-Match → if unchanged, returns 304 (no download) and stays fast. Best of both worlds.
What if I only change a footer widget on a single page? Purge everything?
No. Purge that specific URL on each layer. WP Rocket, LiteSpeed and Cloudflare all support selective URL purge. Reserve the global purge (purge all) for structural changes: new layout, plugin that affects all pages, theme update. Global purge costs recompile time on the first hit.
Do I have to purge OPcache too?
Only when you deploy new PHP code (plugin update push, edit on functions.php). OPcache is compiled PHP bytecode, not HTML cache. If you only change database content (a new post, a menu), OPcache doesn't need a purge. If you deploy code and DON'T purge OPcache, PHP keeps executing the old code version until PHP-FPM restarts.

Keep reading

More posts you might like