Recently, I have been troubleshooting a caching issue in a multilingual WordPress site.
The site currently uses:
- WordPress
- Polylang
- W3 Total Cache
- W3TC Page Cache: Disk Enhanced
- W3TC Object Cache: Redis
- Chinese site:
www.shuijingwanwq.com - English site:
en.shuijingwanwq.com - Admin domain:
admin.shuijingwanwq.com - Chinese site CDN: EdgeOne
- English site CDN: Cloudflare
The W3 Total Cache Page Cache lifetime is set to:
345600 secondsWhich is:
4 daysTheoretically, as long as a page is not actively purged, an already generated Page Cache file should continue to exist for several days.
However, the actual observed behavior was completely different.
After multiple checks, I found that the cache files basically could not survive for a single day. Even without executing W3TC’s admin Purge All Caches, a large number of Page Cache entries would still suddenly and uniformly expire.
This article records the complete process from the initial symptoms, file system evidence, and WordPress Hooks, to finally identifying two full Page Cache flush paths: Polylang tag synchronization and Yoast SEO Cron.
1. The initial problem: A 4-day TTL is effectively meaningless
In the current W3TC configuration:
pgcache.lifetime = 345600Which is 4 days.
But when checking the Page Cache files, I found that the oldest cache entries were usually less than a day old.
What really matters is not “how many days are written in the W3TC configuration,” but rather:
Actual cache lifetime
=
min(
Configured TTL,
Time of the next premature full purge
)If some WordPress Hook triggers a full Page Cache flush every day, then setting the TTL to 4 days, 7 days, or even longer has no practical effect.
Therefore, the focus of the subsequent investigation shifted to:
Who exactly is prematurely purging the Page Cache?
2. First, confirm what a W3TC “full purge” actually looks like
W3TC currently uses Disk Enhanced Page Cache.
The cache directory is located at:
/data/wwwroot/www.shuijingwanwq.com/wp-content/cache/page_enhanced/When starting the investigation, I noticed a very important detail:
When W3TC performs a full Page Cache flush, it does not immediately rm all cache files. Instead, it first renames the files to:
*_oldFor example:
_index_slash_ssl.htmlWould become:
_index_slash_ssl.html_oldThe same applies to gzip files.
This means:
The ctime of
_oldfiles can serve as crucial evidence for “when a Page Cache expiration occurred.”
This detail later proved to be critical.
3. Catching a full Page Cache flush for the first time
During a live inspection, I found that a large number of cache files were uniformly changed to _old in a very short period.
The scale at the time was approximately:
www:906
en:742Total:
1648Moreover, these files were processed consecutively within a time window of less than 0.1 seconds.
This basically rules out:
- Normal user access;
- Prime;
- A single post update;
- Normal TTL expiration;
- CDN;
- Browser cache.
This behavior is more consistent with W3TC’s own:
Cache_File_Generic::flush()Which is a full processing of the entire Page Cache directory.
4. First root cause: Polylang tag synchronization triggers edited_term
I had just run a tag synchronization script that I regularly use:
php polylang-batch-zh-to-en-tags.phpThe purpose of this script is to:
- Scan Chinese tags;
- Check if corresponding English tags already exist;
- If not, create English language tags;
- Establish Polylang translation relationships between Chinese and English tags;
- Later, go to the admin backend and rename the newly generated English tags to proper English.
The result of an actual run was:
Total source language tags: 9451
New tags processed: 2
Skipped existing translations: 9449It seemed only two new tags were added.
Yet these two tags triggered the expiration of the entire Page Cache.
After further tracing, the call chain became clear.
5. The problem is not just wp_insert_term()
Initially, it is easy to suspect the “creation of new tags” itself.
However, in the current environment, W3 Total Cache 2.10.6 primarily hooks into taxonomy-related actions via:
edited_term
delete_termThey are both directly bound to:
w3tc_flush_posts()And w3tc_flush_posts(), under the current Disk Enhanced Page Cache configuration, does not “purge related posts,” but ultimately enters:
PgCache_Flush::flush()It then executes a full flush of the entire Page Cache.
The problem is:
W3TC does not check what kind of taxonomy is actually being modified.
In other words:
Modifying post tags
Modifying categories
Modifying Polylang's internal term translation taxonomyTo W3TC, these can all be treated as:
All post caches should be flushedThis is unreasonable for my website.
6. Polylang translation relationships make the problem more obscure
This time, it was not simply:
Create a post_tag
→ Full flushThe actual process also involves the taxonomy translation relationships maintained by Polylang itself.
Production runtime confirmed that the taxonomy Polylang currently uses for term translation relationships is:
term_translationsAfter the tag synchronization script creates English tags, it continues to update the Polylang translation groups.
This includes:
save_translations()Subsequently, it enters:
wp_update_term()Thereby triggering:
edited_termAnd W3TC directly executes on edited_term:
w3tc_flush_posts()Additionally, cleaning up certain temporary translation relationships might also enter:
delete_termWhich will again execute:
w3tc_flush_posts()Therefore, on the surface, it was just:
Adding two English tagsBehind the scenes, multiple full Page Cache flush requests may have already been initiated.
7. Why this kind of tag modification does not warrant purging the entire site cache
The business requirements need to be clarified first.
My website has a large number of tags, and the real-time consistency requirement for tag and category modifications on the frontend is not high.
Even if a tag name is modified, I can accept that a very small number of already cached pages continue to display old content for a few hours or even days.
In contrast, what is more important is:
Not purging thousands of already generated Page Cache entries due to a very minor taxonomy operation.
Because the current origin server is not a high-performance server.
Under normal circumstances, the best access path is:
CDN HITIf the CDN misses, then:
CDN MISS
→ W3TC Page Cache HIT
→ Does not enter PHPIf the W3TC Page Cache is entirely purged, it becomes:
CDN MISS
→ W3TC MISS
→ PHP
→ WordPress
→ Redis
→ DatabaseUnder heavy traffic, this cache cold start is clearly more likely to cause CPU fluctuations.
Therefore, I ultimately decided:
Modifications to category, post_tag, and Polylang term translation taxonomy will no longer trigger a full W3TC Page Cache flush.
8. First fix: Taxonomy-aware W3TC flush
I did not modify the W3 Total Cache source code, nor did I modify the Polylang source code.
Instead, I continued using an MU Plugin.
The core idea is:
First, remove W3TC’s original:
remove_action( 'edited_term', 'w3tc_flush_posts', 0 );
remove_action( 'delete_term', 'w3tc_flush_posts', 0 );Then register a custom wrapper.
The wrapper determines based on the taxonomy:
category
post_tag
term_translationsFor these taxonomies:
Do not execute full w3tc_flush_posts()For other taxonomies, it continues to call the original W3TC behavior.
The purpose of this is not to “disable all term cache flushing,” but rather:
Only narrow down the scope of taxonomies that are confirmed not to require a full site purge.
9. Did not immediately declare success, but verified with real production workflows
After deploying the code, I did not immediately batch-create data for testing. Instead, I directly verified it using the daily blog publishing workflow.
To objectively observe the cache, I additionally added a read-only tool:
page-cache-snapshotIt gathers statistics for each Host’s:
- Number of Active HTML files;
<1 天;1~2 天;2~3 天;3~4 天;>=4 天;- Oldest active cache time;
- Number of
_oldfiles; - New
_oldappearing in the last 5 / 15 / 60 minutes.
Additionally, a temporary tracer was deployed:
w3tc-full-flush-tracer.phpWhenever:
w3tc_flush_postsOr:
w3tc_flush_allOccurs, it records the call stack.
10. Ran a complete real bilingual blog publishing workflow
Subsequently, I fully published a bilingual blog post.
The verification process included:
1. Creating a new Chinese post
Operations included:
- Creating a new post;
- Editing the content;
- Setting the category;
- Setting tags;
- Saving the draft.
Result:
No full Page Cache flush occurred2. Publishing the Chinese post
After publishing, a small number of _old appeared.
Mainly:
Homepage
Feed
Some paginationBut the oldest historical cache still existed.
Tracer:
0This indicates a normal URL-level precise expiration, not a full purge.
3. Running the tag synchronization script again
This time, the script still added two new tags:
Crawler Hints
Website IndexingStatistics:
Total source language tags: 9453
New tags processed: 2
Skipped existing translations: 9451But before and after Page Cache:
www OLD_COUNT:710 → 710
en OLD_COUNT:618 → 618The oldest cache time remained completely unchanged.
Tracer:
0This shows that the first taxonomy fix has taken effect in a real production workflow.
11. Modifying English tags in the backend no longer purges the Page Cache either
Subsequently, I changed:
网站收录To:
Website IndexingThis action triggers:
wp_update_term()
→ edited_termWhich was exactly the path where W3TC previously executed a full purge.
After the modification:
www OLD_CTIME_5M = 0
en OLD_CTIME_5M = 0Tracer:
0The oldest cache was still preserved.
At this point, it can be confirmed:
Backend modifications via
post_tagwill also no longer clear the entire Page Cache.
12. Continuing to verify English post generation and publishing
Afterwards, I continued through the complete English translation workflow.
SlyTranslate creates English draft
English post ID:
27481After creating the English draft:
www oldest cache unchanged
en oldest cache unchanged
Tracer = 0No full purge.
Officially publishing the English post
After publishing the English post, only a very small number of _old appeared:
www:
feed
en:
homepageCorresponding to regular files and gzip files, there were only 4 in total.
Tracer still:
0This indicates that the official publishing of the English post also relied on a small-scale URL purge.
13. Thought the problem was solved, but the next day the cache still didn’t survive 24 hours
At this point, I briefly thought the problem was solved.
But I did not immediately draw a conclusion, and instead kept the tracer running.
When checking again the next day, I found:
AGE_1_TO_2D = 0The oldest cache had reverted to around 17:30 from the previous day.
More surprisingly:
www OLD_COUNT = 11466
en OLD_COUNT = 7620Clearly, another full flush had occurred.
But this time, the tracer was finally in place.
14. Second root cause: Yoast SEO’s daily Cron
The tracer recorded two instances of:
w3tc_flush_postsThe times were:
17:30:05.175
17:30:05.189Both call stacks pointed to the exact same path:
wpseo_detect_default_seo_data
↓
Default_SEO_Data_Cron_Callback_Integration
↓
Options_Helper->set()
↓
WPSEO_Options::save_option()
↓
update_option_wpseo
↓
WPSEO_Utils::clear_cache()
↓
w3tc_flush_posts()
↓
W3TC full Page Cache flushAt this point, the second root cause was very clear.
15. What exactly did Yoast update?
The current Yoast SEO version in the production environment is:
28.4After further inspecting the source code, I found that this Cron only updates two fields:
default_seo_title
default_seo_meta_descThe values in the database at the time were:
default_seo_title=[27481,27474,27472,27465,27463]
default_seo_meta_desc=[27481,27474,27472,27465,27463]This data stores:
Which of the most recent posts still use the default SEO title or meta description.
They are primarily used for alerts in the Yoast backend editor.
They do not directly change:
- Frontend SEO title;
- Meta description;
- Canonical;
- Schema;
- Sitemap;
- Post content;
- Post meta;
- Indexable.
So for these two backend alert fields:
Clearing the entire Page Cache for www + en + adminIs clearly unnecessary.
16. Why does Yoast trigger a full W3TC flush?
In Yoast SEO 28.4:
WPSEO_Utils::clear_cache()The related logic is very simple:
if ( function_exists( 'w3tc_flush_posts' ) ) {
w3tc_flush_posts();
}
elseif ( function_exists( 'wp_cache_clear_cache' ) ) {
wp_cache_clear_cache();
}That is, as long as W3TC is detected:
Yoast option update
→ WPSEO_Utils::clear_cache()
→ w3tc_flush_posts()It does not further check:
- Which wpseo field was actually modified;
- Whether it occurred in a Cron;
- Whether the field affects the frontend;
- Whether it is really necessary to clear all pages.
17. Yoast itself actually actively bypasses this cache flush
Here, a very interesting detail was discovered.
In another internal process, Yoast 28.4 itself uses similar logic:
remove_action(
'update_option_wpseo',
[ 'WPSEO_Utils', 'clear_cache' ]
);
$this->options_helper->set( ... );
add_action(
'update_option_wpseo',
[ 'WPSEO_Utils', 'clear_cache' ]
);Meaning:
Yoast itself knows that not every internal
wpseooption update warrants triggering a full W3TC cache flush.
This provides a solid implementation basis for the subsequent compatibility solution.
18. Second fix: Temporarily remove the callback only during this Yoast Cron
Ultimately, I did not modify:
WPSEO_Utils::clear_cache()Nor did I touch the internal W3TC Page Cache callback.
Instead, I added a standalone MU Plugin:
yoast-w3tc-cache-compat.phpIt only handles:
wpseo_detect_default_seo_dataThis Cron.
The design is:
priority 1
→ Temporarily remove WPSEO_Utils::clear_cache
priority 10
→ Yoast normally executes the default SEO data Cron
priority 999
→ Restore WPSEO_Utils::clear_cacheTherefore, the suppression only exists within:
wpseo_detect_default_seo_dataThis single action cycle.
It will not persist through the entire:
wp-cron.phpRequest lifecycle.
This way, even if there are other events later in the same PHP Cron request, they will not be affected.
19. Also specifically handled the case where W3TC is absent
Yoast’s clear_cache() also has a fallback:
wp_cache_clear_cache()Therefore, the MU Plugin cannot keep the entire Yoast callback removed when W3TC is not loaded.
Ultimately, a condition was added:
function_exists( 'w3tc_flush_posts' )Suppression is only performed when it is confirmed that W3TC is actually present.
This way:
Yoast Cron + W3TC present
→ Temporarily suppress WPSEO_Utils::clear_cacheAnd:
Yoast Cron + W3TC absent
→ Do not interfere with Yoast at allThis prevents accidentally breaking Yoast’s fallback for other caching plugins.
20. Hook status after deployment
Runtime check after deployment:
wpseo_detect_default_seo_data
1
yoast_w3tc_cache_compat_begin_default_seo_data_cron
10
Default_SEO_Data_Cron_Callback_Integration
->detect_default_seo_data_in_recent
999
yoast_w3tc_cache_compat_end_default_seo_data_cronUnder normal requests:
update_option_wpseo
10 | WPSEO_Options::clear_cache
10 | WPSEO_Utils::clear_cache
10 | Other Yoast watchersThis indicates:
The MU Plugin does not delete Yoast’s original callback during the WordPress loading phase.
It only processes it temporarily when actually entering the target Cron.
21. Finally seeing the Page Cache survive past 24 hours for the first time
Checked again on the night of September 8, 2026.
This time, a result that had never appeared before showed up.
Chinese site:
AGE_LT_1D=6989
AGE_1_TO_2D=181English site:
AGE_LT_1D=4785
AGE_1_TO_2D=147The oldest caches were still respectively:
www:
2026-09-07 17:30:23
en:
2026-09-07 17:30:35The tracer still only had the remaining records from before the previous day’s fix:
2No new records.
This was also the first time since this investigation began that I truly saw:
AGE_1_TO_2D > 0In other words:
For the first time, the Page Cache genuinely crossed the 24-hour lifetime threshold.
This indicates that at least the two main paths that were prematurely purging the Page Cache every day are no longer resetting the cache daily.
22. A verification point that remains not fully closed
Although this Yoast Cron’s next run has advanced from:
2026-09-08To:
2026-09-09Indicating that the Cron executed normally that day.
The tracer also had no new records.
But this time:
default_seo_title
default_seo_meta_descThe two arrays happened to have no changes.
They were still:
[27481,27474,27472,27465,27463]WordPress’s:
update_option()If the old and new values are exactly the same, it will not actually execute a database update, nor will it trigger:
update_option_wpseoSo the most rigorous conclusion at present is:
The Yoast Cron compatibility MU Plugin has passed deployment stability and natural Cron regression tests, and the Page Cache has survived past 24 hours for the first time.
But the strongest piece of evidence is still missing:
default_seo_* values change
↓
update_option_wpseo actually executes
↓
WPSEO_Utils::clear_cache is temporarily suppressed
↓
Tracer still does not increment
↓
Page Cache continues to surviveI did not manually modify the database to create test conditions for this step.
I prefer to wait for a natural content update to trigger it.
23. Biggest takeaway from this investigation: Don’t just stare at the TTL
The most memorable part of this is not a specific piece of MU Plugin code.
But rather:
The configured cache lifetime does not equal the actual cache lifetime.
For example:
W3TC Page Cache TTL = 4 daysOnly means:
Cache is allowed to live up to 4 daysIt does not mean it can actually live for 4 days.
If:
Day 1 Polylang edited_term
→ Full flush
Day 2 Yoast Cron
→ Full flushThen the actual cache lifetime might only be:
Less than 24 hoursIn this case, increasing the TTL is meaningless.
What really should be investigated is:
Who is prematurely clearing the cache?24. Page Cache, Object Cache, and CDN must be evaluated in layers
This investigation also reiterates that caching issues cannot simply be attributed to “Redis” or “W3TC”.
At the very least, we must distinguish between:
Browser cache
CDN
W3TC Page Cache
W3TC Object Cache / Redis
WordPress PHP Runtime
Polylang
NginxBoth issues this time belonged to:
W3TC Page Cache active expirationNot:
Redis Object Cache data pollutionNor:
CDN node cache errorIf from the very beginning you just see “the page is slow again,” and then directly clear Redis, clear the CDN, or Purge All Caches, it will be very difficult to ever know who actually caused the problem.
25. The two compatibility layers currently remain independent
Ultimately, I did not stuff all the compatibility code into a single MU Plugin.
They are now respectively:
polylang-w3tc-cache-compat.phpResponsible for:
Polylang taxonomy
↔
W3 Total CacheAnd:
yoast-w3tc-cache-compat.phpResponsible for:
Yoast SEO default SEO data Cron
↔
W3 Total CacheDoing this has several benefits:
- Single responsibility;
- Easy to disable individually;
- Easy to roll back;
- Can be individually reviewed when upgrading a certain plugin;
- When new issues arise, different compatibility logic is not mixed together.
26. Need to continue observing
Currently, we cannot conclude from this that:
There will absolutely never be any W3TC full Page Cache flush in the futureA more accurate statement is:
Two main premature expiration paths that actually occurred have been identified and fixed.
I am temporarily keeping:
w3tc-full-flush-tracer.phpTo continue observing:
w3tc_flush_posts
w3tc_flush_allIf other plugins, Crons, or backend operations trigger a full flush in the future, it can continue to be located via the call stack.
Additionally, we need to continue observing:
AGE_1_TO_2D
AGE_2_TO_3D
AGE_3_TO_4DThe ultimate goal is to confirm that normal page cache can indeed gradually approach the currently configured:
4-day TTLInstead of being prematurely purged by other hidden paths again.
27. The next round of verification will use this very article
After this article is published, I plan to directly use it as the next round of production testing subject.
If new verification results need to be added later, I will just normally edit and update this already published article.
Then continue to observe:
Article update
↓
Does W3TC only perform precise related URL purges
↓
Do other old Page Caches continue to surviveAt the same time, publishing this article will also change the recent posts collection.
The next time:
wpseo_detect_default_seo_dataWhen the natural Cron runs again:
default_seo_title
default_seo_meta_descAre more likely to undergo real changes.
If at that time:
Option updates normally
Tracer still has no new w3tc_flush_posts
Historical Page Cache still existsThen this Yoast compatibility fix can complete the final piece of the real production loop.
Summary
On the surface, this issue was:
W3 Total Cache has a 4-day Page Cache configured, but why does the cache always fail to survive a single day?
Ultimately, it turned out not to be a TTL configuration error, but rather that at least two premature expiration paths existed:
First:
Polylang tag/category translation relationships
→ edited_term / delete_term
→ W3TC w3tc_flush_posts()
→ Full Page Cache flushSecond:
Yoast SEO
wpseo_detect_default_seo_data Cron
→ update_option_wpseo
→ WPSEO_Utils::clear_cache()
→ w3tc_flush_posts()
→ Full Page Cache flushBoth issues were addressed with minimal compatibility scopes via independent MU Plugins.
After the fix, within the real bilingual blog publishing workflow:
- Chinese draft;
- Chinese publish;
- Tag synchronization;
- Tag renaming;
- English translation draft;
- Official English publish;
None of them triggered an abnormal full Page Cache flush anymore.
Subsequently, the Page Cache also truly showed the following for the first time:
AGE_1_TO_2D > 0For caching issues in production environments, I am now increasingly inclined to adopt a slower but more reliable approach:
First leave evidence
→ Confirm which cache layer it is
→ Find the real Hook
→ Make minimal modifications
→ Verify with real business workflows
→ Continue observingInstead of immediately doing the following as soon as old content, CPU fluctuations, or cache anomalies are seen:
Purge All Caches
Clear Redis
Clear CDNThe latter might make the problem temporarily disappear, but it easily clears away the true root cause along with it.
需要长期技术维护或远程问题排查?
我是拥有 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

