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

From CPU Alerts to Dual-Domain Pre-Caching: A 96-Hour Cache Optimization Case Study With W3 Total Cache, EdgeOne, and Cloudflare

Figure 4: W3TC ultimately uses 300 seconds × 7 pages and switches to the combined Chinese and English Sitemap

作者:

在

On July 28, 2026, my WordPress server triggered a CPU alert again.

I had already gone through multiple rounds of troubleshooting for CPU alerts recently, but this time, instead of continuing to investigate “exactly which process spiked the CPU at that moment,” I decided to approach the problem from a different direction:

Re-enable W3 Total Cache’s page cache preload, and minimize the number of times real users trigger dynamic PHP page generation when accessing cold pages.

In the first phase, I only enabled preloading very conservatively. After running for a while, I could already subjectively feel a noticeable drop in the CPU alert frequency.

This does not strictly prove that “the reduction in CPU alerts was directly caused by preloading,” but it at least indicates that the current cache adjustments did not introduce any new significant overhead, making it worthwhile to continue optimizing in this direction.

Ultimately, I adjusted the caching architecture to:

                 User
                  │
         ┌────────┴────────┐
         ↓                 ↓
      EdgeOne          Cloudflare
       www                en
       96h                96h
         │                 │
         └────────┬────────┘
                  ↓
               W3TC
                96h
                  ↑
          Cache Preload
           300 seconds × 7
                  ↑
       w3tc-preload-sitemap.xml
           www + en combined list
                  ↑
         Automatically regenerated hourly

This article documents the entire process, from the initial 900 秒 × 5 页 to the gradual adjustment toward a combined preloading system for both Chinese and English domains.


1. Why I Re-enabled W3 Total Cache Preloading

W3 Total Cache’s Page Cache can save the HTML pages dynamically generated by WordPress.

If a valid cache for a page already exists, subsequent visits do not need to go through the complete chain every time:

Plaintext
Nginx
→ PHP-FPM
→ WordPress
→ Plugins
→ Database
→ HTML output

However, there is a problem:

Page cache is typically built only after the first visit.

For a website with a large number of existing articles, if many caches expire or are cleared, a sudden surge of cold page visits within a short period could集中触发 PHP page generation.

W3 Total Cache’s Cache Preload is designed specifically to generate these page caches in advance.

The official W3 Total Cache support documentation also explains that Cache Preload proactively generates page caches based on an XML Sitemap, where:

  • Update Interval: the wait time between preload batches;
  • Pages per interval: the number of pages processed per batch;
  • Sitemap URL: the Sitemap used for preloading.

The default values are 900 seconds and 10 pages per batch, respectively. (BoldGrid)


2. Phase 1: Conservative Run with 900 Seconds × 5 Pages

Before re-enabling it, my Cache Preload was turned off:

Plaintext
Automatically prime the page cache: Off
Update interval: 900 seconds
Pages per interval: 10
Sitemap URL: Empty
Figure 1: Before re-enabling, W3 Total Cache's Cache Preload was turned off
Figure 1: Before re-enabling, W3 Total Cache’s Cache Preload was turned off

Because the server had just experienced a CPU alert, I did not use the default 10 pages per batch. Instead, I initially set:

Plaintext
Automatically prime the page cache: On

Update interval:
900 seconds

Pages per interval:
5

Sitemap URL:
https://www.shuijingwanwq.com/sitemap_index.xml

I also kept the following two options turned off:

Plaintext
Preload cache upon publishing a post: Off

Preload cache upon updating a post: Off
Figure 2: Phase 1 used a low-intensity preload of 900 seconds × 5 pages
Figure 2: Phase 1 used a low-intensity preload of 900 seconds × 5 pages

Theoretically, this means:

Plaintext
Every 15 minutes: 5 pages
Every hour: 20 pages
Every day: 480 pages

The goal was not to quickly warm up the entire site, but to first verify:

Whether continuously building the Page Cache without adding significant instantaneous pressure to the server could improve the CPU alert situation.

After running for a while, I actually observed a noticeable decrease in the CPU alert frequency, so I decided to continue optimizing.


3. New Problems Exposed by the 12-Hour Cache Lifetime

At that time, W3 Total Cache had:

Plaintext
Maximum lifetime of cache objects:
43200 seconds

Which is:

Plaintext
12 hours

But the preload rate was only:

Plaintext
20 pages / hour

So, in 12 hours, it could theoretically only process:

Plaintext
20 × 12
= 240 pages

However, the website actually has far more than 240 URLs.

This led me to start thinking:

If the entire Sitemap contains thousands of pages, the page cache only lasts 12 hours, and it takes several days for preloading to complete a full cycle, should I extend the cache lifetime?

It is worth noting that W3TC does not restart from the first page of the Sitemap every 12 hours.

The preload process continues moving forward.

So the real problem is not:

Plaintext
Always only caching the first 240 pages

But rather:

Plaintext
Some caches built earlier have naturally expired
↓
The preload task is still scanning subsequent URLs
↓
It has not yet reached the earlier URLs again

This made me decide to further calculate the actual size of the Sitemap.


4. The First Count Yielded 13,847 URLs, But It Was Wrong

Initially, after recursively counting both Sitemaps, I got:

Plaintext
www: 10696
en : 4428

Merged and deduplicated:
13847

What was even stranger was the domain distribution:

Plaintext
www Sitemap:

7565  media.shuijingwanwq.com
3131  www.shuijingwanwq.com

Clearly, this was incorrect.

Later, I found that the issue was the XML Sitemap contained not only:

XML
<url>
    <loc>https://www.shuijingwanwq.com/...</loc>
</url>

but also image information:

XML
<image:image>
    <image:loc>https://media.shuijingwanwq.com/...</image:loc>
</image:image>

The first version of the counting script counted all XML elements named loc, so:

Plaintext
image:loc

were also mistakenly counted as page URLs.

In fact, it is normal for media.shuijingwanwq.com to appear in the image information of an article’s Sitemap.

Search engines support referencing images on independent domains or CDNs within a Sitemap, so:

Plaintext
Article:
www.shuijingwanwq.com

Image:
media.shuijingwanwq.com

is not an SEO anomaly in itself.

What we actually need to preload are HTML pages, not these image URLs.


5. After Correcting the Count: 6,270 Pages Actually Need Processing

I adjusted the counting logic to:

Only read the direct <loc> of each <url> element, ignoring image:loc.

The final result became:

Plaintext
========== www ==========

www final page URLs: 3130

Domain distribution:
3130  www.shuijingwanwq.com


========== en ==========

en final page URLs: 3140

Domain distribution:
3140  en.shuijingwanwq.com


========== Final Summary ==========

www: 3130
en : 3140
Total: 6270
Merged and deduplicated: 6270

This number is much more reasonable.

In other words, the actual scale we need to consider now is:

Plaintext
Chinese site: 3130
English site: 3140

Total:
6270 pages

6. Why We Cannot Continue Using Only www’s sitemap_index.xml

At this point, there was another issue.

The website already uses two language domains:

Plaintext
Chinese:
https://www.shuijingwanwq.com/sitemap_index.xml

English:
https://en.shuijingwanwq.com/sitemap_index.xml

However, W3 Total Cache’s Cache Preload configuration only has one:

Plaintext
Sitemap URL

Therefore, if I continued to enter:

Plaintext
https://www.shuijingwanwq.com/sitemap_index.xml

then our designed preload system would actually only cover the Chinese site.

To avoid relying on the compatibility of cross-domain nested Sitemaps, I ultimately did not let W3TC handle the two Sitemap Indexes itself. Instead, I created a flat combined Sitemap specifically for W3TC:

Plaintext
https://www.shuijingwanwq.com/w3tc-preload-sitemap.xml

It directly contains:

Plaintext
3130 www pages
+
3140 en pages

Moreover, the Chinese and English URLs are arranged alternately:

Plaintext
www URL 1
en URL 1
www URL 2
en URL 2
www URL 3
en URL 3
...

This prevents a situation where, once a preload cycle begins, it continuously processes only the Chinese site for a long time before finally reaching the English site.


7. The Combined Sitemap Cannot Be a Static File

If I generate it once today:

Plaintext
w3tc-preload-sitemap.xml

and later continue to:

  • publish Chinese articles;
  • publish English translations;
  • add new categories;
  • add new tags;
  • add new series;
  • delete content;

then this combined Sitemap will quickly become outdated.

So the final solution is:

Plaintext
www Yoast Sitemap ──┐
                    ├── Read automatically
en Yoast Sitemap ───┘
                         ↓
               Regenerate combined Sitemap
                         ↓
             w3tc-preload-sitemap.xml
                         ↓
                    W3TC Preload

The combined Sitemap is regenerated once every hour.


8. Reading the Origin Sitemap Directly Instead of Fetching via CDN

Here, I added another layer of processing.

The generation script does not directly access the public CDN, but rather:

Bash
--resolve www.shuijingwanwq.com:443:127.0.0.1
--resolve en.shuijingwanwq.com:443:127.0.0.1

That is:

Plaintext
Script
↓
ECS local Nginx
↓
Latest Yoast Sitemap

Without going through:

Plaintext
EdgeOne
Cloudflare

This way, even if the CDN TTL is later extended to 4 days, the combined Sitemap generation task still reads the latest Sitemap from the origin server, rather than a potentially stale version that might still exist on the CDN.

The first official generation result:

Plaintext
www   : 3130
en    : 3140
total : 6270

output:
/data/wwwroot/www.shuijingwanwq.com/w3tc-preload-sitemap.xml

size  : 546232 bytes

status: OK

Further verification:

Plaintext
Total URLs: 6270
Unique URLs: 6270

en.shuijingwanwq.com: 3140
www.shuijingwanwq.com: 3130

Both the origin server and public access returned:

Plaintext
HTTP=200
content_type=text/xml
size=546232

9. Hourly Automatic Sync Using flock to Prevent Task Overlap

Next, I created:

Plaintext
/etc/cron.d/swq-w3tc-preload-sitemap

With the following content:

Bash
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

17 * * * * root /usr/bin/flock -n /run/lock/swq-w3tc-preload-sitemap.lock /usr/local/bin/swq-w3tc-preload-sitemap.py >> /var/log/swq-w3tc-preload-sitemap.log 2>&1

That is:

Plaintext
17th minute of every hour
↓
Acquire flock lock
↓
Read www Sitemap
↓
Read en Sitemap
↓
Regenerate combined Sitemap
↓
Verify success
↓
Atomically replace old file

Manually simulating Cron:

Plaintext
Exit code: 0

Log:

Plaintext
========== W3TC Combined Sitemap Generation ==========

www   : 3130
en    : 3140
total : 6270

output:
/data/wwwroot/www.shuijingwanwq.com/w3tc-preload-sitemap.xml

size  : 546232 bytes
time  : 23.77s
status: OK

Using:

Plaintext
flock -n

has another benefit:

If the previous round took an abnormally long time to execute, the next round will not launch a second identical task.


10. W3 Total Cache Finally Adjusted to 96 Hours

After determining the total Sitemap size, I changed:

Plaintext
Maximum lifetime of cache objects

from:

Plaintext
43200 seconds
= 12 hours

to:

Plaintext
345600 seconds
= 96 hours
= 4 days
Figure 3: W3 Total Cache page cache maximum lifetime adjusted to 345600 seconds, or 4 days
Figure 3: W3 Total Cache page cache maximum lifetime adjusted to 345600 seconds, or 4 days

Then, Cache Preload was changed from Phase 1’s:

Plaintext
900 seconds
5 pages
www Sitemap

to:

Plaintext
Update interval:
300 seconds

Pages per interval:
7

Sitemap URL:
https://www.shuijingwanwq.com/w3tc-preload-sitemap.xml
Figure 4: W3TC ultimately uses 300 seconds × 7 pages and switches to the combined Chinese and English Sitemap
Figure 4: W3TC ultimately uses 300 seconds × 7 pages and switches to the combined Chinese and English Sitemap

Instant preloading upon publishing and updating posts remains disabled:

Plaintext
Preload cache upon publishing a post: Off
Preload cache upon updating a post: Off

This ensures that all background proactive warming maintains a relatively stable pace, rather than triggering a massive amount of extra tasks when I frequently edit historical articles.


11. Why 300 Seconds × 7 Pages Was the Final Choice

Now:

Plaintext
Every 5 minutes:
7 pages

Every hour:
7 × 12
= 84 pages

Every day:
84 × 24
= 2016 pages

It theoretically takes the following time to complete a full cycle of 6270 URLs:

Plaintext
6270 ÷ 84
≈ 74.64 hours

Which is:

Plaintext
Approximately 3.11 days

And the W3TC Page Cache lifetime is:

Plaintext
96 hours
= 4 days

The theoretical margin is approximately:

Plaintext
96 - 74.64
≈ 21.36 hours

Compared to generating dozens of pages at once, I prefer:

Generating fewer pages each time, but running more evenly.

Because the primary goal of this optimization was to reduce CPU spikes, not to achieve the fastest possible full-site warm-up and artificially create another source of CPU spikes.


12. 8064 URLs Is Not a Hard Limit That Forces Parameter Changes

According to:

Plaintext
300 seconds × 7

the theoretical processing capacity over 96 hours is:

Plaintext
84 × 96
= 8064 URL

But later I realized:

8064 is not a hard limit that forces an immediate increase in preload speed once the URL count exceeds it.

Assuming the Sitemap grows to:

Plaintext
10000 URL

in the future. A full cycle would take:

Plaintext
10000 ÷ 84
≈ 119 hours
≈ 5 days

which already exceeds 96 hours.

However, a real website does not rely solely on Preload to build its cache.

The actual environment also involves:

Plaintext
Real user visits
Search engine crawling
CDN caching
Existing W3TC Page Cache
Cache Preload

participating together.

So even if the Sitemap reaches 10,000, it does not mean that a large number of those pages will simultaneously be in a cold cache state.

More importantly:

Popular pages are inherently more likely to have their caches rebuilt by real requests, while for cold pages that rarely receive visits, there is no need to significantly increase server load just to keep them in a “theoretically warm cache” state at all times.

Therefore, whether I need to increase from:

Plaintext
300 × 7

in the future will not be judged solely based on the Sitemap URL count.

What really matters is:

  • Whether CPU usage is stable;
  • CPU alert frequency;
  • Actual PHP-FPM load;
  • CDN origin pull status;
  • Actual Page Cache hit rate.

13. EdgeOne’s www Also Extended to 4 Days

After adjusting W3TC, I also changed the EdgeOne HTML page cache TTL for the Chinese site:

Plaintext
www.shuijingwanwq.com

from the original 12 hours to:

Plaintext
4 days
= 96 hours

The rules still exclude dynamic and backend entry points, such as:

Plaintext
wp-admin
wp-login.php
wp-json
feed
xmlrpc.php
wp-cron.php

Ultimately:

Plaintext
Node cache TTL:
Custom time

Time:
4 days

Force caching:
Off
Figure 5: EdgeOne HTML page node cache TTL adjusted to 4 days
Figure 5: EdgeOne HTML page node cache TTL adjusted to 4 days

EdgeOne’s official documentation states that the node cache TTL determines how long a resource stays in the node cache; a cache hit can reduce origin pull requests and lower the burden on the origin server. The documentation also reminds users that EdgeOne has a cold file eviction mechanism, so the TTL is the maximum cache duration, and it does not mean that all cold files will definitely be stored for the full 4 days on every node. (Tencent Cloud)

This further illustrates that:

Cache design does not need to pursue keeping all 6,000-plus pages in an absolutely warm state at all times and on all nodes.

What we are really pursuing is reducing the overall origin pull ratio and sudden origin pull pressure.


14. Cloudflare’s en Also Set to 4 Days

The English site:

Plaintext
en.shuijingwanwq.com

uses Cloudflare.

In the corresponding HTML Cache Rule:

Plaintext
Cache eligibility:
Eligible for cache

Edge TTL is set to:

Plaintext
Ignore cache-control header, use this TTL

4 days
Figure 6: Cloudflare English site HTML Edge TTL adjusted to 4 days
Figure 6: Cloudflare English site HTML Edge TTL adjusted to 4 days

Cloudflare officially defines Edge TTL as the duration a resource is cached in Cloudflare’s edge network, which can override the origin server’s TTL via Cache Rules. (Cloudflare Docs)

What I modified here is the Edge TTL, not the browser cache TTL.

Therefore:

Plaintext
Cloudflare node caches HTML for 4 days

does not mean:

Plaintext
User's Firefox / Chrome
must keep the HTML for 4 days

These are two different cache layers.


15. Final Cache Architecture

At this point, the entire caching system became:

                    User request
                       │
            ┌──────────┴──────────┐
            ↓                     ↓
         EdgeOne              Cloudflare
           www                    en
          96h                    96h
            │                     │
            └──────────┬──────────┘
                       ↓
                    Nginx
                       ↓
                W3 Total Cache
                     96h
                       ↑
                  Cache Preload
                   300 seconds × 7
                       ↑
             W3TC Combined Sitemap
                  6270 URLs
                       ↑
          ┌────────────┴────────────┐
          ↓                         ↓
    www Yoast Sitemap        en Yoast Sitemap

          Automatically synced at the 17th minute of every hour

The core of this solution is not “the longer the cache, the better,” but rather establishing three layers of defense:

Plaintext
Layer 1:
CDN aims for direct HITs

Layer 2:
When CDN pulls from origin, it aims to HIT W3TC

Layer 3:
Only when neither layer has an available cache
does it actually let PHP + WordPress dynamically generate the page

At the same time, through low-batch, high-frequency preloading, cold pages gradually enter the Page Cache.


16. Why I Did Not Clear the Existing Cache This Time

After completing all the adjustments, I did not execute:

Plaintext
W3TC Purge All Caches
Cloudflare Purge Everything
EdgeOne full-site refresh

Because the goal this time was originally:

to reduce cold caches.

If I had immediately cleared all existing caches right after setting up the preload system, it would have artificially created a scenario with:

Plaintext
Massive CDN MISSes
+
Massive W3TC MISSes
+
Concentrated PHP page generation

This is completely contrary to the direction of this optimization.

So, after the configuration adjustments were completed, I chose to let the new rules run naturally.


17. Another Issue with 96-Hour Caching: Purging After Content Updates

After extending the cache duration from 12 hours to 4 days, there is another cost that must be recognized:

The longer the cache, the longer the potential duration that stale content continues to exist on CDN nodes.

EdgeOne’s official documentation explicitly states that if an origin resource is updated and the node cache needs to be updated immediately, you can proactively perform a cache purge.

Cloudflare similarly defines Edge TTL as the lifecycle of the edge node cache; once the setting overrides the origin TTL, special attention must be paid to the CDN purge mechanism when content is updated.

Therefore, this 96-hour scheme does not mean:

Plaintext
No need to consider cache purging when publishing or modifying articles in the future

Quite the opposite.

For WordPress pages, we still need to ensure moving forward that:

Plaintext
Content modification
↓
W3TC invalidates the corresponding page cache
↓
CDN can also promptly invalidate or purge the corresponding URL

However, this is a “targeted cache purge after content update” issue, which is a different problem from the “reducing origin server pressure caused by random cold pages” solved in this article.

This round of adjustments will not expand its scope for now.


18. Phased Conclusion

This adjustment initially started very simply:

After the CPU alerted again, I decided to temporarily stop analyzing the root cause of the alert and instead turn W3 Total Cache’s Cache Preload back on.

Phase 1:

Plaintext
W3TC TTL: 12 hours
Preload: 900 seconds × 5
www only

After running it, I actually observed a decrease in the CPU alert frequency.

So I further organized the Sitemaps, fixed the image:loc counting issue, and ultimately achieved:

Plaintext
www: 3130
en: 3140

Combined:
6270 URLs

Then, through an automated script and Cron, it dynamically syncs every hour:

Plaintext
w3tc-preload-sitemap.xml

The final caching strategy was adjusted to:

Plaintext
W3TC Page Cache:
96 hours

W3TC Cache Preload:
300 seconds × 7

EdgeOne: www
96 hours

Cloudflare: en
96 hours

The biggest change this time was not simply changing the TTL from:

Plaintext
12 hours

to:

Plaintext
96 hours

but rather gradually shifting from the originally passive:

Plaintext
User visit
→ Cold page cache
→ WordPress temporarily generates

to:

Plaintext
Continuous low-speed background warming
+
Long-period CDN caching
+
Long-period W3TC Page Cache
+
Real visits supplementing warm cache

I also no longer pursue a theoretical goal:

All URLs must always be in a warm cache state.

As long as it can ultimately achieve:

Most real requests ending at the CDN or W3TC layer as much as possible, while keeping proactive preloading running at low intensity and stably, without creating new CPU spikes for the sake of caching itself.

then this solution has achieved its goal.

Next, I plan to temporarily stop adjusting parameters and let this:

Plaintext
CDN 96h
+
W3TC 96h
+
300 seconds × 7
+
www/en combined preloading

run normally for a few days, and then judge whether further optimization is necessary based on the CPU alert frequency and actual origin pull status.

系列导航

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系