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

Why W3 Total Cache Expires Daily Despite a 4-Day Setting: A Polylang and Yoast SEO Troubleshooting Log

Why W3 Total Cache Expires Daily Despite a 4-Day Setting: A Polylang and Yoast SEO Troubleshooting Log

作者:

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:

Plaintext
345600 seconds

Which is:

Plaintext
4 days

Theoretically, 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:

Plaintext
pgcache.lifetime = 345600

Which 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:

Plaintext
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:

Plaintext
/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:

Plaintext
*_old

For example:

Plaintext
_index_slash_ssl.html

Would become:

Plaintext
_index_slash_ssl.html_old

The same applies to gzip files.

This means:

The ctime of _old files 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:

Plaintext
www:906
en:742

Total:

Plaintext
1648

Moreover, 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:

Plaintext
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:

Bash
php polylang-batch-zh-to-en-tags.php

The purpose of this script is to:

  1. Scan Chinese tags;
  2. Check if corresponding English tags already exist;
  3. If not, create English language tags;
  4. Establish Polylang translation relationships between Chinese and English tags;
  5. Later, go to the admin backend and rename the newly generated English tags to proper English.

The result of an actual run was:

Plaintext
Total source language tags: 9451
New tags processed: 2
Skipped existing translations: 9449

It 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:

Plaintext
edited_term
delete_term

They are both directly bound to:

Plaintext
w3tc_flush_posts()

And w3tc_flush_posts(), under the current Disk Enhanced Page Cache configuration, does not “purge related posts,” but ultimately enters:

Plaintext
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:

Plaintext
Modifying post tags
Modifying categories
Modifying Polylang's internal term translation taxonomy

To W3TC, these can all be treated as:

Plaintext
All post caches should be flushed

This is unreasonable for my website.


6. Polylang translation relationships make the problem more obscure

This time, it was not simply:

Plaintext
Create a post_tag
→ Full flush

The 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:

Plaintext
term_translations

After the tag synchronization script creates English tags, it continues to update the Polylang translation groups.

This includes:

Plaintext
save_translations()

Subsequently, it enters:

Plaintext
wp_update_term()

Thereby triggering:

Plaintext
edited_term

And W3TC directly executes on edited_term:

Plaintext
w3tc_flush_posts()

Additionally, cleaning up certain temporary translation relationships might also enter:

Plaintext
delete_term

Which will again execute:

Plaintext
w3tc_flush_posts()

Therefore, on the surface, it was just:

Plaintext
Adding two English tags

Behind 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:

Plaintext
CDN HIT

If the CDN misses, then:

Plaintext
CDN MISS
→ W3TC Page Cache HIT
→ Does not enter PHP

If the W3TC Page Cache is entirely purged, it becomes:

Plaintext
CDN MISS
→ W3TC MISS
→ PHP
→ WordPress
→ Redis
→ Database

Under 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:

PHP
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:

Plaintext
category
post_tag
term_translations

For these taxonomies:

Plaintext
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:

Plaintext
page-cache-snapshot

It 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 _old files;
  • New _old appearing in the last 5 / 15 / 60 minutes.

Additionally, a temporary tracer was deployed:

Plaintext
w3tc-full-flush-tracer.php

Whenever:

Plaintext
w3tc_flush_posts

Or:

Plaintext
w3tc_flush_all

Occurs, 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:

Plaintext
No full Page Cache flush occurred

2. Publishing the Chinese post

After publishing, a small number of _old appeared.

Mainly:

Plaintext
Homepage
Feed
Some pagination

But the oldest historical cache still existed.

Tracer:

Plaintext
0

This 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:

Plaintext
Crawler Hints
Website Indexing

Statistics:

Plaintext
Total source language tags: 9453
New tags processed: 2
Skipped existing translations: 9451

But before and after Page Cache:

Plaintext
www OLD_COUNT:710 → 710
en  OLD_COUNT:618 → 618

The oldest cache time remained completely unchanged.

Tracer:

Plaintext
0

This 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:

Plaintext
网站收录

To:

Plaintext
Website Indexing

This action triggers:

Plaintext
wp_update_term()
→ edited_term

Which was exactly the path where W3TC previously executed a full purge.

After the modification:

Plaintext
www OLD_CTIME_5M = 0
en  OLD_CTIME_5M = 0

Tracer:

Plaintext
0

The oldest cache was still preserved.

At this point, it can be confirmed:

Backend modifications via post_tag will 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:

Plaintext
27481

After creating the English draft:

Plaintext
www oldest cache unchanged
en oldest cache unchanged
Tracer = 0

No full purge.


Officially publishing the English post

After publishing the English post, only a very small number of _old appeared:

Plaintext
www:
feed

en:
homepage

Corresponding to regular files and gzip files, there were only 4 in total.

Tracer still:

Plaintext
0

This 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:

Plaintext
AGE_1_TO_2D = 0

The oldest cache had reverted to around 17:30 from the previous day.

More surprisingly:

Plaintext
www OLD_COUNT = 11466
en  OLD_COUNT = 7620

Clearly, 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:

Plaintext
w3tc_flush_posts

The times were:

Plaintext
17:30:05.175
17:30:05.189

Both call stacks pointed to the exact same path:

Plaintext
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 flush

At 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:

Plaintext
28.4

After further inspecting the source code, I found that this Cron only updates two fields:

Plaintext
default_seo_title
default_seo_meta_desc

The values in the database at the time were:

Plaintext
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:

Plaintext
Clearing the entire Page Cache for www + en + admin

Is clearly unnecessary.


16. Why does Yoast trigger a full W3TC flush?

In Yoast SEO 28.4:

PHP
WPSEO_Utils::clear_cache()

The related logic is very simple:

PHP
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:

Plaintext
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:

PHP
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 wpseo option 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:

Plaintext
WPSEO_Utils::clear_cache()

Nor did I touch the internal W3TC Page Cache callback.

Instead, I added a standalone MU Plugin:

Plaintext
yoast-w3tc-cache-compat.php

It only handles:

Plaintext
wpseo_detect_default_seo_data

This Cron.

The design is:

Plaintext
priority 1
→ Temporarily remove WPSEO_Utils::clear_cache

priority 10
→ Yoast normally executes the default SEO data Cron

priority 999
→ Restore WPSEO_Utils::clear_cache

Therefore, the suppression only exists within:

Plaintext
wpseo_detect_default_seo_data

This single action cycle.

It will not persist through the entire:

Plaintext
wp-cron.php

Request 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:

PHP
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:

PHP
function_exists( 'w3tc_flush_posts' )

Suppression is only performed when it is confirmed that W3TC is actually present.

This way:

Plaintext
Yoast Cron + W3TC present
→ Temporarily suppress WPSEO_Utils::clear_cache

And:

Plaintext
Yoast Cron + W3TC absent
→ Do not interfere with Yoast at all

This prevents accidentally breaking Yoast’s fallback for other caching plugins.


20. Hook status after deployment

Runtime check after deployment:

Plaintext
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_cron

Under normal requests:

Plaintext
update_option_wpseo

10 | WPSEO_Options::clear_cache
10 | WPSEO_Utils::clear_cache
10 | Other Yoast watchers

This 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:

Plaintext
AGE_LT_1D=6989
AGE_1_TO_2D=181

English site:

Plaintext
AGE_LT_1D=4785
AGE_1_TO_2D=147

The oldest caches were still respectively:

Plaintext
www:
2026-09-07 17:30:23

en:
2026-09-07 17:30:35

The tracer still only had the remaining records from before the previous day’s fix:

Plaintext
2

No new records.

This was also the first time since this investigation began that I truly saw:

Plaintext
AGE_1_TO_2D > 0

In 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:

Plaintext
2026-09-08

To:

Plaintext
2026-09-09

Indicating that the Cron executed normally that day.

The tracer also had no new records.

But this time:

Plaintext
default_seo_title
default_seo_meta_desc

The two arrays happened to have no changes.

They were still:

Plaintext
[27481,27474,27472,27465,27463]

WordPress’s:

PHP
update_option()

If the old and new values are exactly the same, it will not actually execute a database update, nor will it trigger:

Plaintext
update_option_wpseo

So 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:

Plaintext
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 survive

I 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:

Plaintext
W3TC Page Cache TTL = 4 days

Only means:

Plaintext
Cache is allowed to live up to 4 days

It does not mean it can actually live for 4 days.

If:

Plaintext
Day 1 Polylang edited_term
→ Full flush

Day 2 Yoast Cron
→ Full flush

Then the actual cache lifetime might only be:

Plaintext
Less than 24 hours

In this case, increasing the TTL is meaningless.

What really should be investigated is:

Plaintext
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:

Plaintext
Browser cache
CDN
W3TC Page Cache
W3TC Object Cache / Redis
WordPress PHP Runtime
Polylang
Nginx

Both issues this time belonged to:

Plaintext
W3TC Page Cache active expiration

Not:

Plaintext
Redis Object Cache data pollution

Nor:

Plaintext
CDN node cache error

If 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:

Plaintext
polylang-w3tc-cache-compat.php

Responsible for:

Plaintext
Polylang taxonomy

W3 Total Cache

And:

Plaintext
yoast-w3tc-cache-compat.php

Responsible for:

Plaintext
Yoast SEO default SEO data Cron

W3 Total Cache

Doing 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:

Plaintext
There will absolutely never be any W3TC full Page Cache flush in the future

A more accurate statement is:

Two main premature expiration paths that actually occurred have been identified and fixed.

I am temporarily keeping:

Plaintext
w3tc-full-flush-tracer.php

To continue observing:

Plaintext
w3tc_flush_posts
w3tc_flush_all

If 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:

Plaintext
AGE_1_TO_2D
AGE_2_TO_3D
AGE_3_TO_4D

The ultimate goal is to confirm that normal page cache can indeed gradually approach the currently configured:

Plaintext
4-day TTL

Instead 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:

Plaintext
Article update

Does W3TC only perform precise related URL purges

Do other old Page Caches continue to survive

At the same time, publishing this article will also change the recent posts collection.

The next time:

Plaintext
wpseo_detect_default_seo_data

When the natural Cron runs again:

Plaintext
default_seo_title
default_seo_meta_desc

Are more likely to undergo real changes.

If at that time:

Plaintext
Option updates normally
Tracer still has no new w3tc_flush_posts
Historical Page Cache still exists

Then 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:

Plaintext
Polylang tag/category translation relationships
→ edited_term / delete_term
→ W3TC w3tc_flush_posts()
→ Full Page Cache flush

Second:

Plaintext
Yoast SEO
wpseo_detect_default_seo_data Cron
→ update_option_wpseo
→ WPSEO_Utils::clear_cache()
→ w3tc_flush_posts()
→ Full Page Cache flush

Both 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:

Plaintext
AGE_1_TO_2D > 0

For caching issues in production environments, I am now increasingly inclined to adopt a slower but more reliable approach:

Plaintext
First leave evidence
→ Confirm which cache layer it is
→ Find the real Hook
→ Make minimal modifications
→ Verify with real business workflows
→ Continue observing

Instead of immediately doing the following as soon as old content, CPU fluctuations, or cache anomalies are seen:

Plaintext
Purge All Caches
Clear Redis
Clear CDN

The 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