/ 7 min read

What Actually Clears W3 Total Cache's Page Cache (It's More Than You Think)

In my last post I dug into what W3 Total Cache actually stores on disk, raised the cache lifetime to 30 days, and set a crawler loose to keep a large site permanently primed. Problem solved, I thought.

Then I checked the numbers. One evening the cache held around 3,000 pages. By morning it was down to 1,000. With a 30-day lifetime and garbage collection running once a day, expiry couldn’t explain it. Something was actively deleting cached pages faster than the crawler could rebuild them.

So I went through W3TC’s source and traced every code path that can clear the page cache. What I found applies to any site running W3TC - and a couple of the triggers are surprising enough that I think every W3TC user should know about them.

First, rule out the usual suspects

My first guess was another plugin doing full flushes behind my back - Yoast has a reputation for this sort of thing. So I grepped every plugin on the site for w3tc_flush_all() and friends. Result: complete exoneration. Every full-flush caller was gated behind a manual admin action:

  • Yoast SEO only flushes on plugin activation/deactivation - never on post saves or sitemap updates.
  • OMGF only flushes after an admin saves its settings.
  • Elementor only flushes when maintenance mode changes.
  • Post Types Order flushes on a manual drag-reorder, Email Address Encoder on its settings save.

Nothing scheduled, nothing on post updates. If your cache is disappearing, the odds are the culprit isn’t a rogue plugin - it’s W3TC’s own triggers doing exactly what they’re configured to do, on more content than you realise.

The purge policy applies to every post type

The Purge Policy settings say: “Specify the pages and feeds to purge when posts are created, edited, or comments posted.” Posts, right?

No. In Util_Environment::is_flushable_post(), the only post types excluded are revision and attachment. Every published post of any post type goes through the full purge policy on save: WooCommerce products, events, portfolio items, custom post types of every kind. And “on save” means any save - not just an editor clicking Update, but every programmatic wp_update_post() from an importer, a stock sync, a feed integration or a cron job.

A single save also purges more than the post. With a fairly standard policy, PgCache_Flush::flush_post() builds a URL list of:

  • the post’s own URLs,
  • for a custom post type, up to 10 pages of its archive (that’s the “Purge home” option - for CPTs it purges the post type archive, not your front page),
  • and, whenever the sitemap regex option is set, the entire cached sitemap group, every time, for every post.

Mostly this is what you want - stale product pages are worse than cold ones. But it means the volume of cache churn scales with how much of your content changes per day, not with how much editors touch. On the site in question, yacht listings sync from an external feed, so every price change was quietly purging a page, ten archive pages and all the sitemaps. A busy WooCommerce store does the same thing every time stock levels tick over.

Saving a menu wipes the entire cache

While reading Util_AttachToActions.php (where W3TC attaches its “content changed” hooks), I found the list of triggers that call w3tc_flush_posts() - which for the page cache means flush everything:

add_action( 'switch_theme', 'w3tc_flush_posts', 0, 0 );
add_action( 'wp_update_nav_menu', 'w3tc_flush_posts', 0, 0 );
add_action( 'edited_term', 'w3tc_flush_posts', 0, 0 );
add_action( 'delete_term', 'w3tc_flush_posts', 0, 0 );

Read that middle pair again. Editing or deleting any taxonomy term flushes the whole page cache. Rename a category, fix a typo in a tag, tidy up some old terms: the entire site’s cache is gone, once per term. Same for saving a menu. If someone does a bit of housekeeping in the admin at 11pm, that’s your mysterious overnight cache wipe right there.

There’s logic to it - a renamed category can appear on any page, and W3TC can’t know which - but the pricing is brutal, and nothing in the UI warns you.

Two mercies, confirmed in the same file:

  • Creating a term doesn’t flush anything - create_term isn’t hooked. Plugins that add new terms as content comes in do no damage.
  • Post meta updates don’t flush either. Only actual post saves (save_post, trash, delete) trigger the purge machinery. Code that skips wp_update_post() when nothing has really changed causes zero cache activity - worth knowing if you write anything that syncs content.

If you ever need to do bulk term maintenance, W3TC provides a pre-flush filter that lets you hold the flood back and flush once at the end:

add_filter( 'w3tc_preflush_posts', '__return_false' );
// ... rename / merge / delete terms ...
remove_filter( 'w3tc_preflush_posts', '__return_false' );
w3tc_flush_posts(); // one flush instead of hundreds

The double flush

To see the purge activity instead of guessing at it, I added an activity log to my W3TC Cache Insights plugin that records every flush event W3TC fires (v0.2.0 also keeps per-day counters, because a rolling feed scrolls away fast on a busy site). It immediately showed something odd: two “flush post” events for every item my importer updated, about a minute apart.

The cause is a footgun that applies to any plugin that both saves posts and clears cache - a very common combination. W3TC hooks save_post at priority 0, so the moment code calls wp_update_post(), W3TC has already queued a purge - before the plugin has written the post’s meta fields and taxonomy terms. When the plugin then calls w3tc_flush_post() itself at the end of processing (as many do, to be “cache friendly”), that’s purge number two. Everything from the previous section doubles: two purges of the page, twenty archive-page purges, two sitemap flushes. Per item.

The fix is the per-post version of the same pre-flush filter, wrapped around the save:

add_filter( 'w3tc_preflush_post', '__return_false' );
wp_update_post( $args );
remove_filter( 'w3tc_preflush_post', '__return_false' );

// ... write meta, set terms, index ...

w3tc_flush_post( $post_id, true ); // one purge, after the data is complete

Now the only purge happens when the post is actually finished. If you use a plugin that updates content automatically and the flush log shows doubles, this is almost certainly why.

Purges don’t happen when you call them

This was the biggest surprise of the lot, and it invalidated a “fix” I’d already shipped.

To soften the purge churn, I’d made our importer re-prime each page after flushing it: purge the page, then fire a non-blocking loopback request so the page is regenerated and back in the cache before a real visitor arrives. Sensible, right?

Except W3TC doesn’t delete anything when you call w3tc_flush_post(). It queues the purge and executes it later - on the shutdown hook at priority 100000 (CacheFlush.php):

add_action( 'shutdown', array( $this, 'execute_delayed_operations' ), 100000, 0 );

So the sequence was actually: queue the purge, fire the loopback request - which gets served the old cached page, because it still exists - and then, at shutdown, W3TC deletes both the stale page and the “freshly primed” copy, which was the same stale file all along. The re-prime was a no-op. The page ended up uncached anyway, waiting for the next visitor to pay the regeneration cost.

This matters to anyone doing cache warming around purges: any priming you do inside the same request as the purge is wasted. Once you know the timing, the fix is mechanical: prime on shutdown too, at a later priority than 100000. And since W3TC fires public actions (w3tc_flush_post, w3tc_flush_url) for every purge, one small listener covers every purge on the site, whoever triggered it:

add_action( 'w3tc_flush_post', function ( $post_id ) {
    // resolve the permalink now and queue it...
}, 20 );

add_action( 'shutdown', 'my_prime_queued_urls', 200000 ); // after W3TC's 100000

Queueing has a bonus: it dedupes. A bulk operation that flushes the same archive page fifty times in one request primes it once.

I’ve published this as a tiny free plugin, W3TC Flush Re-prime - no settings, no stored data. Activate it and purged pages come back into the cache within seconds instead of waiting for a crawler or an unlucky visitor.

The checklist

If your W3TC page cache keeps shrinking and the lifetime maths doesn’t add up:

  1. Check the lifetime on the server, not from memory - wp w3-total-cache option get pgcache.lifetime. (Mine really was 30 days this time. It isn’t always.)
  2. Log the flushes. Cache Insights records every flush event with what triggered it, plus daily totals. A “Flush all posts” entry at 11pm points straight at a term edit or a menu save.
  3. Remember the purge policy hits every post type - and each save also purges CPT archive pages and all sitemaps. Anything that saves posts programmatically (importers, stock syncs, feed integrations) drives cache churn you never see in the editor.
  4. Watch for the double flush. If a plugin both saves posts and calls w3tc_flush_post() itself, suppress W3TC’s automatic one with w3tc_preflush_post.
  5. Don’t prime inline after a purge. It does nothing - the purge hasn’t happened yet. Prime on shutdown after priority 100000, or just use the plugin.

The cache on my site now holds steady, the flush volume halved overnight, and the pages that do get purged are back in the cache before anyone notices they left.

Robert Went

Freelance PHP developer with 19+ years experience, specialising in technical SEO, WordPress, and Laravel.

More about Rob