没有不值得去解决的问题,也没有不值得去学习的技术!

WordPress Custom Taxonomy Updates Invalidate the W3TC Full Page Cache: A Real Production Fix for Series

WordPress Custom Taxonomy Updates Invalidate the W3TC Full Page Cache: A Real Production Fix for Series

作者:

Recently, I had just resolved two types of W3 Total Cache Page Cache full flush issues in a WordPress multilingual environment:

One was triggered by Polylang during tag, category, and language association operations via edited_term / delete_term;

The other came from Yoast SEO’s daily Cron, which called w3tc_flush_posts() when updating the wpseo option.

The fix at that time was not to completely disable W3 Total Cache’s cache clearing, but to add a taxonomy-aware compatibility layer.

For taxonomies confirmed to accept eventual consistency, we no longer call W3TC’s site-wide:

PHP
w3tc_flush_posts();

For other unverified taxonomies, the original W3TC full flush behavior was retained.

This design was intentionally conservative.

But on September 10, 2026, after modifying the blog “series” description, I discovered again:

The W3TC Page Cache was almost entirely cleared again.

This time, the root cause was traced to another custom taxonomy:

Plaintext
series

This article documents the complete process from discovery and localization to modifying the Git repository, deploying to the production environment, and real-world verification.


1. After modifying the series description, the Page Cache was almost entirely cleared

That day, I performed two types of backend operations in sequence:

  1. Modified the blog series description;
  2. Updated a Code Snippet.

After completing these operations, I was concerned that the recently fixed W3TC Page Cache full flush issue had reoccurred, so I immediately ran the existing read-only snapshot tool:

Bash
page-cache-snapshot 20260910-after-series-and-code-snippet-update

The results were obvious.

The Chinese site had only:

Plaintext
ACTIVE_HTML_COUNT=16
AGE_LT_1D=16
AGE_1_TO_2D=0
AGE_2_TO_3D=0

The English site had only:

Plaintext
ACTIVE_HTML_COUNT=37
AGE_LT_1D=37
AGE_1_TO_2D=0
AGE_2_TO_3D=0

While in the last normal snapshot from the previous day:

Plaintext
www ACTIVE_HTML=10957
en  ACTIVE_HTML=9980

This equates to:

Plaintext
www:10957 → 16
en : 9980 → 37

The Page Cache accumulated over more than two days had essentially disappeared.

This was no longer a standard URL-level cache miss.

Therefore, it could first be confirmed:

Another W3TC full Page Cache flush had occurred.


2. Full Flush Tracer increased from 2 to 4 entries

To troubleshoot such issues, I had previously deployed a temporary MU Plugin:

Plaintext
wp-content/mu-plugins/w3tc-full-flush-tracer.php

It specifically logs the call chains of:

Plaintext
w3tc_flush_posts
w3tc_flush_all

Checking the logs:

Bash
wc -l /var/log/w3tc-full-flush-tracer.log

Previously, it consistently showed:

Plaintext
2

Both entries were from the previously identified Yoast SEO Cron.

But this time it changed to:

Plaintext
4

This means there were two new instances of w3tc_flush_posts().

Both new call chains originated from the backend:

Plaintext
POST /wp-admin/edit-tags.php

wp_update_term()

edited_term

polylang_w3tc_cache_compat_flush_posts_for_term()

w3tc_flush_posts()

Therefore, Yoast Cron could be ruled out this time.

However, since I had also just updated a Code Snippet, I couldn’t immediately determine:

Whether it was caused by modifying the series or by updating the Code Snippet?

So I continued checking the Nginx backend access logs.


3. Nginx logs confirm: both requests were taxonomy=series

The backend logs clearly show:

Plaintext
POST /wp-admin/edit-tags.php
taxonomy=series
tag_ID=42451

And:

Plaintext
POST /wp-admin/edit-tags.php
taxonomy=series
tag_ID=42453

These two terms are respectively:

Plaintext
42451
A Tour of Go 多语言翻译项目

And:

Plaintext
42453
A Tour of Go Multilingual Translation Project

In other words, both instances of w3tc_flush_posts() corresponded to the Chinese and English series I had just modified.

The Code Snippet could be ruled out.

Figure 1: Backend Nginx logs, both edit-tags.php requests explicitly point to taxonomy=series.
Figure 1: Backend Nginx logs, both edit-tags.php requests explicitly point to taxonomy=series.

I then checked the taxonomies currently registered in WordPress:

Bash
wp eval --allow-root '
foreach ( ["series", "series_group"] as $taxonomy ) {
    $obj = get_taxonomy( $taxonomy );

    printf(
        "%s\tpublic=%s\tshow_ui=%s\thierarchical=%s\tobject_type=%s\trewrite=%s\n",
        $taxonomy,
        $obj->public ? "yes" : "no",
        $obj->show_ui ? "yes" : "no",
        $obj->hierarchical ? "yes" : "no",
        implode(",", $obj->object_type),
        is_array($obj->rewrite) ? ($obj->rewrite["slug"] ?? "") : ""
    );
}
'

Results:

Plaintext
series
public=yes
show_ui=yes
hierarchical=no
object_type=post
rewrite=series

series_group
public=yes
show_ui=yes
hierarchical=yes
object_type=series_grouping
rewrite=series-category

The two terms just modified also clearly belong to:

Plaintext
taxonomy=series
count=65

Thus, the root cause could be further narrowed down.


4. Why the existing MU Plugin didn’t prevent this full flush

Previously, to handle the taxonomy cache compatibility issue between Polylang and W3TC, I had deployed:

Plaintext
wp-content/mu-plugins/polylang-w3tc-cache-compat.php

This plugin removes W3TC’s original taxonomy-blind callback:

PHP
remove_action( 'edited_term', 'w3tc_flush_posts', 0 );
remove_action( 'delete_term', 'w3tc_flush_posts', 0 );

And replaces it with its own wrapper.

The taxonomies that allowed eventual consistency at the time were:

PHP
$taxonomies = array( 'category', 'post_tag' );

It also dynamically reads the term translation taxonomy used by Polylang itself.

If the current taxonomy is in this list:

PHP
return;

It prevents:

PHP
w3tc_flush_posts();

But for other taxonomies:

PHP
if ( function_exists( 'w3tc_flush_posts' ) ) {
    w3tc_flush_posts();
}

The original W3TC full flush behavior is retained.

So this time, the MU Plugin didn’t fail to work.

Instead, it executed exactly as originally designed:

Plaintext
Edit series

wp_update_term()

edited_term

taxonomy=series

Not in suppression list

fallback

w3tc_flush_posts()

Full Page Cache flush

The only problem was:

There was previously no real-world production evidence to prove that series should also be added to this list.

Now there is.


5. A series description is not worth clearing the entire cache of two sites

The series actually modified this time each contained:

Plaintext
count=65

Meaning there are 65 articles under this series.

But only the series description text was modified.

The result, however, was:

Plaintext
www ACTIVE_HTML:10957 → 16
en  ACTIVE_HTML:9980 → 37

This means that updating two series terms wiped out the active page cache of nearly 20,000 pages across the Chinese and English sites.

For the current website, this cost is clearly too high.

Especially since the site has enabled:

Plaintext
W3 Total Cache Page Cache
Disk Enhanced
4 天 TTL
Prime

Page Cache itself is a crucial layer for origin server load control.

Therefore, the more sensible strategy here is still:

Allow series to use eventual consistency, rather than continuing to trigger site-wide cache flushes.


6. Why only add series, and not series_group while at it

This time, I still did not adopt:

Since they are all series-related taxonomies, just add them all.

In actual operation:

Plaintext
series
object_type=post

While:

Plaintext
series_group
object_type=series_grouping

The two are not exactly the same thing.

And this real-world production incident only proved that:

Plaintext
series

triggers an unnecessary full Page Cache flush during normal blog series edits.

There is no actual evidence to prove that:

Plaintext
series_group

should also be modified at the same time.

Therefore, adhering to the principle of minimal changes:

Plaintext
category       suppress full flush
post_tag       suppress full flush
series         suppress full flush
Polylang term translation taxonomy
               suppress full flush

series_group   keep original logic
other taxonomies keep original logic

7. This time, the production server was not modified directly

There is another detail worth recording during this process.

When I first prepared the fix, I almost modified it directly on the production server:

Plaintext
wp-content/mu-plugins/polylang-w3tc-cache-compat.php

But doing so would create a very troublesome issue:

Plaintext
Production

Git Repository

If the production environment is modified first and the Git sync is forgotten, or if Git deploys back later, version drift can easily occur.

Therefore, I ultimately strictly returned to:

Plaintext
Local Git repository

Modify source code

Update regression tests

Local testing

commit

push

Deploy committed Git version

SHA256 verification

Production runtime verification

8. The local modification only adds one taxonomy

Local repository:

Plaintext
shuijingwan/wordpress-polylang-w3tc-cache-compat

The core modification in the source code is just one line.

Originally:

PHP
$taxonomies = array( 'category', 'post_tag' );

Modified to:

PHP
$taxonomies = array( 'category', 'post_tag', 'series' );

Also updated:

Plaintext
tests/term-page-cache-flush-suppression-test.php

Test coverage:

Plaintext
edited category
edited post_tag
edited series
edited term_translations

And deletion scenarios:

Plaintext
delete category
delete post_tag
delete series

At the same time, the original unknown taxonomy fallback must still be retained:

Plaintext
custom_taxonomy
→ w3tc_flush_posts()

Local check results:

Plaintext
No syntax errors detected
OK
OK
git diff --check PASS

9. Committed and pushed to GitHub

This commit:

Plaintext
60f0873a55424231c476d62bdafefe3f57a2c545

Commit message:

Plaintext
fix: 抑制 series 更新触发 W3TC 全量页面缓存清理

After pushing:

Plaintext
HEAD
=
origin/master
=
60f0873a55424231c476d62bdafefe3f57a2c545

Local source code SHA256:

Plaintext
614a4f9724fd8f5bdde9cf81ca5d0b992c654c1d0532d65432c872e13fcdd7b9

Only after this step was completed did the production deployment begin.


10. The first production deployment actually failed

Before deployment, the committed Git version was uploaded to the server:

Plaintext
/tmp/polylang-w3tc-cache-compat.php

Uploaded file SHA256:

Plaintext
614a4f9724fd8f5bdde9cf81ca5d0b992c654c1d0532d65432c872e13fcdd7b9

Old production file SHA256:

Plaintext
4d9c9119212bcc3ca15fcf999d57b2920ea0f699311f4ec03b03bff9730b90d5

Backed up first:

Plaintext
wp-content/mu-plugins/polylang-w3tc-cache-compat.php.bak-20260910-165622

First execution:

Bash
cp -p "$SOURCE" "$FILE"

The server prompted:

Plaintext
cp:是否覆盖...?

After confirming, I continued checking.

Found that:

Plaintext
Production SHA256
is still:
4d9c9119212bcc3ca15fcf999d57b2920ea0f699311f4ec03b03bff9730b90d5

And at runtime it was still:

Plaintext
series SUPPRESS_FULL_FLUSH=NO

That is to say:

The first deployment did not actually take effect.

This is why production deployment cannot only look at:

Plaintext
命令没有报错

It must also check:

Plaintext
File SHA256
+
PHP syntax
+
Runtime behavior

11. Deployment was only truly completed after using /bin/cp

Then explicitly bypassed the shell alias:

Bash
/bin/cp -pf "$SOURCE" "$FILE"

Checked again:

Plaintext
PRODUCTION SHA256
614a4f9724fd8f5bdde9cf81ca5d0b992c654c1d0532d65432c872e13fcdd7b9

PHP syntax:

Plaintext
No syntax errors detected

Runtime:

Plaintext
category      SUPPRESS_FULL_FLUSH=YES
post_tag      SUPPRESS_FULL_FLUSH=YES
series        SUPPRESS_FULL_FLUSH=YES
series_group  SUPPRESS_FULL_FLUSH=NO
Figure 2: Runtime check after production deployment, series=YES, while series_group=NO.
Figure 2: Runtime check after production deployment, series=YES, while series_group=NO.

At the same time:

Plaintext
Full Flush Tracer = 4

This indicates that the deployment action itself did not trigger a new full cache flush.


12. Two consecutive real edits to series

After deployment, I didn’t just do CLI simulations.

To get as close to real-world usage as possible, I made two consecutive actual modifications to the series content in the WordPress backend.

Before the fix:

Plaintext
Tracer = 4

If the fix had not taken effect, two real modifications should theoretically have continued to:

Plaintext
4 → 5 → 6

But the actual check still showed:

Plaintext
Tracer = 4

No new additions of:

Plaintext
w3tc_flush_posts

Cache snapshot comparison:

Plaintext
www
ACTIVE_HTML=86->170
OLD_COUNT=16->16
OLDEST_EPOCH=1789029661->1789029661

English site:

Plaintext
en
ACTIVE_HTML=91->153
OLD_COUNT=24->24
OLDEST_EPOCH=1789029648->1789029648

These metrics are very clean.

First:

Plaintext
ACTIVE_HTML

Continued to grow normally.

Second:

Plaintext
OLD_COUNT

Did not increase due to the two series modifications.

Finally:

Plaintext
OLDEST_EPOCH

Was not reset at all.

Figure 3: After two consecutive real modifications to series, ACTIVE_HTML continued to increase, while OLD_COUNT and OLDEST_EPOCH both remained unchanged.
Figure 3: After two consecutive real modifications to series, ACTIVE_HTML continued to increase, while OLD_COUNT and OLDEST_EPOCH both remained unchanged.

Therefore, this fix can be officially concluded as:

Production environment verification passed.


13. But the series page still shows the old description

After fixing the full flush, I checked the series page again.

Found that:

The page still displayed an earlier version of the series description.

However, it must be emphasized here:

This is not a new issue that appeared after this series full flush fix.

Before this fix, I had already modified the series description, but the frontend did not correctly display the latest content at that time either.

That is to say:

Plaintext
Old description issue

predates this series full flush fix itself.

Therefore, it cannot be written as:

Plaintext
Suppress full flush

Causes series page to become outdated

This causal relationship does not exist.


14. Further confirmation: the current Page Cache is indeed saving a historical old version

Later, I checked the series description in the current WordPress runtime.

The database / WordPress current value already contains:

Plaintext
Brazilian Portuguese
Dutch
Italian
Spanish
Turkish

and other new language entries.

But the current W3TC Page Cache HTML still only has the earlier version:

Plaintext
简体中文
日语
德语
法语

And the generation time of the current series Page Cache file is:

Plaintext
2026-09-10 17:11:21

That is to say:

Although the current Page Cache was regenerated today, the generated content is still the old version of the series description.

This further indicates that:

The old description issue is not simply because the Page Cache wasn’t cleared after disabling the full flush this time.

It is another pre-existing cache consistency issue.


15. This part belongs to the previously confirmed Object Cache issue

For the current website, this type of phenomenon has been investigated before.

The site uses:

Plaintext
W3 Total Cache Object Cache
+
Redis

It has happened before that:

Plaintext
Database has been updated

WordPress certain objects still read old values

Regenerated Page Cache continues to save old content

That is to say:

Plaintext
Page Cache

sometimes just re-saves the old objects already read by the PHP runtime into HTML.

Therefore, since the series description remained an old value this time, I did not dig deeper.

This belongs to the previously confirmed and accepted Object Cache / Redis consistency issue.

The current strategy remains:

It is acceptable to temporarily have the old series description, and not proactively expand the cache clearing scope for this type of low-priority content.

This article will also not expand on further Object Cache troubleshooting.


16. The two issues need to be clearly separated

This time, it is especially easy to mix the two issues together.

The first issue is:

Plaintext
Edit series

edited_term

w3tc_flush_posts()

Site-wide Page Cache is cleared

This issue has been fixed and verified in this article.

The second issue is:

Plaintext
series description has been updated

Object Cache / Redis may still retain old term data

Page Cache continues to write old description when regenerated

This issue existed previously and has already been confirmed.

Currently temporarily accepted.

So the final state is:

Plaintext
Full Page Cache flush
→ Resolved

series description cache lag
→ Historically known issue
→ Currently accepted
→ Not processing further for now

After separating these two issues, the entire causal relationship becomes much clearer.


17. Why it’s still worth writing a separate article this time

On the surface, this was just adding one more to the array:

PHP
'series'

But the real value isn’t this single line of code.

Rather, the production environment proved once again that:

If W3TC’s taxonomy cache invalidation logic is too broad, it can easily turn a very small backend operation into a site-wide cache cold start.

It had been previously confirmed for:

Plaintext
category
post_tag
Polylang term translation taxonomy

This time, adding:

Plaintext
series

And this time, we also retained the fallback for:

Plaintext
series_group
other unknown taxonomies

This is less risky than simply and crudely doing:

Plaintext
所有 edited_term 都 return

for all of them.


18. Current taxonomy cache strategy

So far, this MU Plugin’s handling of term updates can be summarized as:

Plaintext
category
→ Allow eventual consistency
→ No full flush

post_tag
→ Allow eventual consistency
→ No full flush

series
→ Allow eventual consistency
→ No full flush

Polylang term translation taxonomy
→ Allow eventual consistency
→ No full flush

Other taxonomies:

Plaintext
→ Keep W3TC original fallback
→ w3tc_flush_posts()

The benefit of doing this is:

Only expanding the scope that has passed real-world production verification.

If new taxonomies are discovered later, we will continue to follow:

Plaintext
Discover issue

Confirm taxonomy

Confirm business impact

Only add verified taxonomies

Rather than disabling all W3TC clearing triggered by term updates at once.


19. Final result

This new Page Cache full flush issue was ultimately confirmed as follows.

Trigger condition

Modifying:

Plaintext
taxonomy=series

For example:

Plaintext
A Tour of Go 多语言翻译项目

‘s series description.

Call chain

Plaintext
wp_update_term()

edited_term

polylang_w3tc_cache_compat_flush_posts_for_term()

series not recognized as eventual-consistency taxonomy

w3tc_flush_posts()

www + en full Page Cache flush

Fix

Add:

Plaintext
series

Into:

PHP
$taxonomies = array(
    'category',
    'post_tag',
    'series'
);

series_group remains unchanged.

Git commit

Plaintext
60f0873a55424231c476d62bdafefe3f57a2c545

Production file SHA256

Plaintext
614a4f9724fd8f5bdde9cf81ca5d0b992c654c1d0532d65432c872e13fcdd7b9

Real-world production verification

Two consecutive series edits:

Plaintext
Tracer:4 → 4

Chinese site:

Plaintext
ACTIVE_HTML=86->170
OLD_COUNT=16->16
OLDEST_EPOCH=1789029661->1789029661

English site:

Plaintext
ACTIVE_HTML=91->153
OLD_COUNT=24->24
OLDEST_EPOCH=1789029648->1789029648

No site-wide Page Cache flush occurred again.


20. Summary

This issue made me confirm once again:

Cache compatibility issues cannot just be looked at in terms of “whether the page is updated”; first, it must be clarified which cache layer exhibited what behavior.

In fact, two different things existed simultaneously this time:

Plaintext
series update triggering site-wide Page Cache flush

And:

Plaintext
series description having historical Object Cache lag

The former causes a large number of pages to cold start again, so it must be prioritized.

The latter only means that some low-priority taxonomy content temporarily retains an old version, which is currently acceptable.

The final strategy adopted is:

Plaintext
Resolve the full flush that affects site-wide stability

Rather than:

Plaintext
To make one series description refresh immediately
Clearing the entire Page Cache or Redis

This also still aligns with the current site’s cache optimization principles:

Prioritize resolving issues that affect correctness, stability, and overall performance; for acceptable eventual consistency, do not expand the cache clearing scope just to pursue “immediate updates”.

From Polylang tags and categories, to Yoast SEO Cron, and now this series, a clearer handling approach has gradually taken shape:

Plaintext
First confirm the actual trigger chain

Distinguish Page Cache / Object Cache

Keep W3TC default behavior as fallback

Only minimally suppress taxonomies that have been verified in real-world scenarios

Then verify via production snapshots and Tracer

Compared to “fixing all cache issues” at once, this method of confirming and converging one by one is more suitable for a production WordPress site that already has real traffic.

系列导航

需要长期技术维护或远程问题排查?

我是拥有 15+ 年经验的 PHP / Go 后端工程师,长期关注已有系统维护、Bug 修复、性能优化、服务器排查、WordPress 网站维护和小功能迭代。

如果你的项目遇到以下情况,可以先从一次小问题排查开始合作:

  • ✅ PHP / Laravel / Yii2 老项目无人维护
  • ✅ Go / Gin 后端接口需要排查或优化
  • ✅ WordPress 网站访问慢、报错或插件冲突
  • ✅ Nginx / MySQL / Redis / Linux 服务器异常
  • ✅ CDN / Cloudflare / DNS / HTTPS 配置问题
  • ✅ 需要长期远程技术支持或兼职维护

更多介绍请查看:关于我 & 合作

微信:13980074657
邮箱:shuijingwanwq@gmail.com
Telegram:@shuijingwan
GitHub:https://github.com/shuijingwan