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:
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:
Automatically prime the page cache: Off
Update interval: 900 seconds
Pages per interval: 10
Sitemap URL: Empty

Because the server had just experienced a CPU alert, I did not use the default 10 pages per batch. Instead, I initially set:
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:
Preload cache upon publishing a post: Off
Preload cache upon updating a post: Off

Theoretically, this means:
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:
Maximum lifetime of cache objects:
43200 seconds
Which is:
12 hours
But the preload rate was only:
20 pages / hour
So, in 12 hours, it could theoretically only process:
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:
Always only caching the first 240 pages
But rather:
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:
www: 10696
en : 4428
Merged and deduplicated:
13847
What was even stranger was the domain distribution:
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:
<url>
<loc>https://www.shuijingwanwq.com/...</loc>
</url>
but also image information:
<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:
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:
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, ignoringimage:loc.
The final result became:
========== 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:
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:
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:
Sitemap URL
Therefore, if I continued to enter:
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:
https://www.shuijingwanwq.com/w3tc-preload-sitemap.xml
It directly contains:
3130 www pages
+
3140 en pages
Moreover, the Chinese and English URLs are arranged alternately:
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:
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:
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:
--resolve www.shuijingwanwq.com:443:127.0.0.1
--resolve en.shuijingwanwq.com:443:127.0.0.1
That is:
Script
↓
ECS local Nginx
↓
Latest Yoast Sitemap
Without going through:
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:
www : 3130
en : 3140
total : 6270
output:
/data/wwwroot/www.shuijingwanwq.com/w3tc-preload-sitemap.xml
size : 546232 bytes
status: OK
Further verification:
Total URLs: 6270
Unique URLs: 6270
en.shuijingwanwq.com: 3140
www.shuijingwanwq.com: 3130
Both the origin server and public access returned:
HTTP=200
content_type=text/xml
size=546232
9. Hourly Automatic Sync Using flock to Prevent Task Overlap
Next, I created:
/etc/cron.d/swq-w3tc-preload-sitemap
With the following content:
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:
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:
Exit code: 0
Log:
========== 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:
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:
Maximum lifetime of cache objects
from:
43200 seconds
= 12 hours
to:
345600 seconds
= 96 hours
= 4 days

Then, Cache Preload was changed from Phase 1’s:
900 seconds
5 pages
www Sitemap
to:
Update interval:
300 seconds
Pages per interval:
7
Sitemap URL:
https://www.shuijingwanwq.com/w3tc-preload-sitemap.xml

Instant preloading upon publishing and updating posts remains disabled:
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:
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:
6270 ÷ 84
≈ 74.64 hours
Which is:
Approximately 3.11 days
And the W3TC Page Cache lifetime is:
96 hours
= 4 days
The theoretical margin is approximately:
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:
300 seconds × 7
the theoretical processing capacity over 96 hours is:
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:
10000 URL
in the future. A full cycle would take:
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:
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:
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:
www.shuijingwanwq.com
from the original 12 hours to:
4 days
= 96 hours
The rules still exclude dynamic and backend entry points, such as:
wp-admin
wp-login.php
wp-json
feed
xmlrpc.php
wp-cron.php
Ultimately:
Node cache TTL:
Custom time
Time:
4 days
Force caching:
Off

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:
en.shuijingwanwq.com
uses Cloudflare.
In the corresponding HTML Cache Rule:
Cache eligibility:
Eligible for cache
Edge TTL is set to:
Ignore cache-control header, use this TTL
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:
Cloudflare node caches HTML for 4 days
does not mean:
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:
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:
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:
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:
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:
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:
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:
www: 3130
en: 3140
Combined:
6270 URLs
Then, through an automated script and Cron, it dynamically syncs every hour:
w3tc-preload-sitemap.xml
The final caching strategy was adjusted to:
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:
12 hours
to:
96 hours
but rather gradually shifting from the originally passive:
User visit
→ Cold page cache
→ WordPress temporarily generates
to:
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:
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.

