Stop flushing your entire WordPress cache

Philipp Wellmer Philipp Wellmer 9 min read

Somewhere in your site there is a synced pattern. A newsletter signup, say, or the promo box for your current launch. Over the months, editors have dropped it into whatever needed it: a few product pages, some posts, two landing pages. Nobody kept a list. That is the whole point of synced patterns: build once, reuse anywhere, edit in one place.

pattern:1637 post:88 archive:category:5 home

Your cache knows what changed.

Every cached page carries flags for the content it renders. Edit a synced pattern and MilliCache Pro clears just the pages that use it, at the origin and at the edge. Out of the thousands of pages a site caches, only the few that actually changed go cold.

This banner is itself a synced pattern. When we edit it, MilliCache Pro clears exactly the pages that render it. Nothing else moves.

Today you edit it. New wording, new offer, new form endpoint. WordPress saves the pattern, and a question appears that sounds trivial and is not: which cached pages are now stale?

Your caching plugin cannot answer it. The pattern’s usage exists only inside block markup scattered across your content; no archive lists it, no taxonomy groups it, no URL structure betrays where it lives. So if the old offer must not stay visible anywhere, there is exactly one safe move: clear the entire cache. Two thousand warm pages, including the ones currently receiving ad traffic, go cold because one reusable block changed its wording.

If your site leans on shared content, and every block-based site does, this scenario repeats weekly. This post is about why it happens, why it is an architecture problem rather than a settings problem, and how cache flags solve it.

Why full flushes became the default#

A page cache is, at its core, a key-value store. The key is derived from the URL (plus things like cookies or query parameters), and the value is the rendered HTML.

That model is great for serving. It is terrible for invalidation, because the cache knows nothing about what is on those pages.

To be fair, caching plugins have learned to handle the classic case well. Edit a post, and a decent plugin runs a database query and generates the list of URLs that predictably relate to it: the post itself, its archives, the homepage, the feed. That works, because the publishing model’s relationships are queryable. The trouble starts with the questions no such query can answer:

  • Which pages embed this synced pattern?
  • Which pages render this template part?
  • Which pages show this post through a Query Loop block?
  • Which pages feature this product in a “related items” section?

These relationships exist only inside block markup and rendered output, not in any metadata a URL-list query can reach. When the shared thing changes, there is nothing to generate the list from. Plugin authors are left with two options: guess narrowly and risk stale content, which produces support tickets, or clear everything, which produces slow sites, and nobody files a bug report for “my site was briefly slower.” So for shared content, the ecosystem converged on nuking.

The result: on a modern block-based site, the full flush is not the emergency fallback. It is the routine response to editing anything shared.

What invalidation actually needs: a relationship model#

The missing piece is a way to record, at the moment a page is cached, what content that page depends on. If the cache knows “this cached entry contains post 123 and belongs to category 5,” then when post 123 changes, it can look up exactly the affected entries and clear those.

That is what cache flags are. When MilliCache stores a page, it tags the entry with labels describing the content on it:

URLFlags
/blog/my-post/post:123, home
/blog/archive:post, home
/category/tutorials/archive:category:5
/blog/my-post/?utm_source=xpost:123, home

Note the last row. A single URL can produce multiple cache entries (query parameters, cookie variants, mobile variants), and they all carry the same flags. Clearing post:123 catches every variant of every page that contains post 123, without anyone having to enumerate URLs.

MilliCache assigns these flags automatically: post:{id} on singular pages, archive:category:{id} on term archives, archive:author:{id}, date archive flags, home, feed, and so on. When you update a post, MilliCache clears the post’s own flag, the archives it belongs to, the homepage, and the feeds. Nothing else moves. If your site has 2,000 cached pages and you edit one post that appears on six of them, 1,994 pages stay warm.

The MilliCache browser extension reads the flag header for you, so you can check what a page is tagged with and whether it was a hit.

You can watch this happen. Enable debug mode and every cached response tells you what it is tagged with in the response header:

 1X-MilliCache-Flags: post:123, archive:category:5, home

Under the hood: sets, not scans#

“Tag-based invalidation” only works if lookups are cheap in both directions, and this is where storage architecture matters.

MilliCache stores pages in Redis (or ValKey, or any Redis-compatible server). Every flag is a native Redis set whose members are the cache entries carrying that flag. Clearing post:123 means: read one set, delete its members. That is it. No iterating over the whole keyspace, no SCAN over thousands of entries, no “walk every cached file on disk and check its metadata.” The cost of an invalidation scales with the number of affected pages, not with the size of your cache.

Each cache entry also records its own flags, so the relationship is readable from both sides: “which entries have this flag” and “which flags does this entry have” are both single lookups. That second direction is what makes the debug header, the WP-CLI stats filter, and MilliCache Pro’s cache browser possible.

This is the same architectural idea behind surrogate keys at Fastly or cache tags in Varnish and Cloudflare. It is a well-proven pattern at the CDN layer. MilliCache brings it to the WordPress origin, where your content relationships are actually known.

Back to the synced pattern#

With flags, the unanswerable question from the intro becomes a lookup. Tag every cached page with the shared elements it renders, at render time, where that information actually exists, and “which pages are now stale” is something the cache answers itself.

MilliCache Pro’s Block Editor module does exactly this, automatically: it tracks which pages use which synced patterns, Query Loop sources, and Site Editor templates and template parts, and when one changes, clears precisely the pages that render it. The newsletter pattern from the intro sits on twelve pages; those twelve get cleared, the other two thousand stay warm.

+ Call to action
Synced pattern

Only the pages using this pattern were cleared. The rest of the cache stayed warm.

Without that module the flag system is still open to you, and the next section shows how to tag pages from your own code. But tracking block containment reliably means following patterns nested inside template parts and inside other patterns, and where MilliCache cannot know better it stays deliberately conservative: a template part save still clears the site cache, because correctness beats cleverness. MilliCache Pro’s tracking is what removes that last full flush.

Custom flags: your content model, your rules#

Built-in flags cover what WordPress knows about. Custom flags cover what you know about.

Say you run a summer sale and a dozen scattered pages show the sale banner via some condition in your theme. Tag them:

functions.php
 1add_filter( 'millicache_flags_for_request', function ( $flags ) {
 2	if ( my_theme_promo_is_active() ) {
 3		$flags[] = 'promo:summer-sale';
 4	}
 5	return $flags;
 6} );

When the sale ends, you clear it by name. From the MilliCache settings screen, or open the WordPress command palette and start typing: MilliCache registers “Clear cache for flag” and “Expire cache for flag” there, so ending a promotion is a few keystrokes from anywhere in wp-admin. Patterns work too, so promo:* retires every promotion at once.

The same thing from the command line, if you are already in a terminal:

terminal
 1wp millicache clear --flag="promo:summer-sale"

Those are the one-off routes. The same clear exists in PHP as millicache_clear_cache_by_flags(), which is what you reach for when it should happen on its own rather than when someone remembers to do it. That is the more interesting case, because it covers the relationships WordPress cannot see. Take a product that appears in a “featured” carousel on other pages:

functions.php
 1add_filter( 'millicache_flags_related_to_post', function ( $flags, $post ) {
 2	if ( 'product' === $post->post_type && get_post_meta( $post->ID, 'featured', true ) ) {
 3		$flags[] = 'featured';
 4	}
 5	return $flags;
 6}, 10, 2 );

Now updating a featured product also clears every page tagged featured, automatically, as part of the normal save flow.

The mental shift is the important part. You stop thinking in URLs (“which pages do I need to purge?”) and start thinking in content (“this thing changed”). The cache resolves the rest.

Even targeted clearing can be gentler: expire instead of delete#

There is a second decision hidden inside every invalidation: what happens to the visitor who arrives right after?

Delete an entry and the next visitor waits for a full WordPress render. Expire it instead, and MilliCache serves the previous version once more while regenerating in the background. The visitor gets an instant response that is seconds out of date; the visitor after that gets the fresh page. For a typo fix or a price tweak, that trade is almost always right:

 1// true = expire (serve stale once, regenerate in background)
 2millicache_clear_cache_by_post_ids( [ $post_id ], true );
terminal
 1wp millicache clear --flag="post:123" --expire
Tip:

Use expire for routine content updates and deletion only for urgent corrections. Combined with flags, invalidation stops being an event your visitors can feel.

What this means in numbers#

The metric to watch is your cache hit rate over time, not a one-off speed test. A site that full-flushes on every edit shows a sawtooth: hit rate climbs, an editor saves a post, hit rate falls off a cliff, repeat. Each cliff is a period where real visitors got your slowest pages and your server carried load the cache should have absorbed.

With flag-based invalidation the sawtooth flattens, because each edit knocks out a handful of entries instead of all of them. The busier your editorial workflow, the bigger the difference. On a site publishing several times a day, full flushing can mean the cache never actually gets warm.

If you cannot currently see your hit rate, that is a gap worth closing before you tune anything. You cannot improve what you cannot measure.


Try it#

MilliCache assigns flags automatically and gives you the full clearing API out of the box: wp millicache clear --flag=…, the PHP helpers, the millicache_flags_for_request filter, and debug headers to inspect what got tagged. Start with the cache flags introduction and the cache clearing guide.

MilliCache Pro takes flags further where visibility and shared content are concerned: the Cache Entries browser shows every cached page with its flags right in wp-admin, the Block Editor module makes synced pattern and template edits surgical instead of site-wide, Detailed Metrics draws the hit-rate chart discussed above, and the Edge Cache module mirrors flag purges to your CDN so the edge is exactly as fresh as your origin.

Get new articles first

Guides, updates and release notes in your inbox. No spam.

By subscribing you agree to our Privacy Policy.­ Double opt-in, no spam, unsubscribe anytime.

Leave a comment