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

From EdgeOne 524 to Query String Normalization: CDN Cache Optimization for WordPress Posts and Public Listing Pages

Figure 3: The complete structure of the EdgeOne rule after saving. Article detail pages and public list pages share the same rule.

作者:

在

,

Previously, I performed a Query String normalization process for WordPress article detail pages.

When an article detail page includes amp, nonamp, random test parameters, or other meaningless query parameters, the CDN no longer builds a separate cache for each parameter combination. Instead, it uniformly reuses the cache corresponding to the canonical URL. At the same time, when there is a cache miss and a request is sent to the origin, the query parameters are no longer passed to WordPress.

The previous process is documented in this article:

Unifying WordPress Article Query Strings via EdgeOne and Cloudflare

However, a subsequent EdgeOne 524 incident made me realize that processing only article detail pages was insufficient.

A large number of category pages, tag pages, series pages, date archive pages, and paginated URLs are also appended with various query parameters by crawlers. If all these parameters are included in the CDN cache key, they will create massive cache fragmentation and repeatedly trigger dynamic origin requests to WordPress.

This time, I extended the same Query String normalization scheme to WordPress public list pages, completing the configuration and verification on the Chinese site’s EdgeOne and the English site’s Cloudflare, respectively.

1. How the Problem Surfaced

On the evening of August 2, 2026, the Chinese site experienced a surge of EdgeOne 524 errors.

During the incident window, EdgeOne recorded 151 524 errors:

  • Cache MISS: 147
  • Dynamic requests: 4
  • Cache HIT: 0

These requests were not concentrated on a single article but were scattered across the homepage, category pages, tag pages, date archives, and paginated URLs.

The origin Nginx logs simultaneously showed a large number of 499 errors. The requests included the homepage, category pages, and URLs with parameters such as query-62-page, amp, and nonamp.

PHP-FPM was using the following configuration at the time:

  • ECS: 1-core CPU
  • pm = ondemand
  • pm.max_children = 7
  • request_slowlog_timeout = 3s
  • request_terminate_timeout = 600s

The slow log showed that during the incident, 7 PHP-FPM child processes were simultaneously stuck in index.php. The log then showed:

Plaintext
server reached pm.max_children setting (7)

Since the server only has a 1-core CPU, continuing to increase pm.max_children was not a reasonable direction.

Even increasing the number of PHP processes would only cause more processes to contend for the same CPU. What truly needed to be reduced was:

The same page being repeatedly treated as different resources due to different Query Strings, causing CDN cache misses and simultaneous origin requests executing WordPress.

2. Why Query Parameters Amplify Origin Load

The following URLs might actually return the exact same content in WordPress:

Plaintext
https://www.shuijingwanwq.com/tag/cache/
https://www.shuijingwanwq.com/tag/cache/?amp=1
https://www.shuijingwanwq.com/tag/cache/?nonamp=1
https://www.shuijingwanwq.com/tag/cache/?query-62-page=43
https://www.shuijingwanwq.com/tag/cache/?random=value

However, if the CDN includes the full Query String in the cache key by default, they will be treated as multiple distinct cache objects.

Each time a new parameter combination appears for the first time, it can trigger an origin request.

When crawlers simultaneously access a large number of category, tag, paginated, and archive pages, this cache fragmentation can quickly exhaust all PHP-FPM processes.

Therefore, this optimization adopted a two-layer approach:

  1. Query Strings do not participate in the CDN cache key;
  2. On a cache miss during origin fetch, the Query String is directly removed.

The first layer prevents cache fragmentation, while the second prevents meaningless parameters from being passed to WordPress.

3. Which Public List Pages Need Coverage

I first compiled the list pages currently existing on the Chinese site:

Plaintext
https://www.shuijingwanwq.com/
https://www.shuijingwanwq.com/page/4/

https://www.shuijingwanwq.com/category/program-development/
https://www.shuijingwanwq.com/category/program-development/page/2/

https://www.shuijingwanwq.com/tag/cache/
https://www.shuijingwanwq.com/tag/cache/page/2/

https://www.shuijingwanwq.com/series-category/blog-management-practices/

https://www.shuijingwanwq.com/series/wordpress-cdn-global-acceleration-practice/
https://www.shuijingwanwq.com/series/wordpress-cdn-global-acceleration-practice/page/3/

https://www.shuijingwanwq.com/2026/
https://www.shuijingwanwq.com/2026/page/32/

https://www.shuijingwanwq.com/2026/07/
https://www.shuijingwanwq.com/2026/07/page/3/

https://www.shuijingwanwq.com/2026/07/30/

The rules also need to accommodate future pagination:

Plaintext
/series-category/blog-management-practices/page/2/
/2026/07/30/page/2/

Author archives were not included in the scope.

This is a single-author blog, and author archives have been disabled in Yoast SEO, so /author/.../ is not a currently valid public list page.

Only process canonical URLs with a trailing slash

The rules only match URLs ending with /.

URLs without a trailing slash, such as:

Plaintext
/tag/cache
/category/program-development

are first redirected via 301 by WordPress or existing canonicalization logic to:

Plaintext
/tag/cache/
/category/program-development/

They are then processed by the CDN rules.

This remains consistent with the previous handling for article detail pages.

4. Search Pages Must Retain Query Strings

You cannot simply delete query parameters for the homepage and all /page/数字/ URLs.

WordPress search pages rely on the s parameter:

Plaintext
https://www.shuijingwanwq.com/?s=autopoly
https://www.shuijingwanwq.com/page/2/?s=autopoly

The path for search pagination is itself also /page/2/.

If judged solely by the URL path, search pagination could be mistaken for standard homepage pagination, causing s=autopoly to be deleted and ultimately returning a standard article list.

Therefore, the list page branch must additionally require:

Plaintext
Query parameter s does not exist

I also excluded WordPress preview parameters from the list pages:

Plaintext
preview
preview_id
preview_nonce

5. EdgeOne Chinese Site Configuration

Instead of creating two separate rules for article detail pages and public list pages, I merged them into a single rule.

The rule name is:

Plaintext
www Article and Public List Page Query String Normalization

The outermost condition is only:

Plaintext
HOST equals www.shuijingwanwq.com

A limitation encountered during configuration

Initially, I also added at the outermost level:

Plaintext
Request method equals GET, HEAD

However, after adding the request method condition, the EdgeOne interface no longer allowed adding the “Custom Cache Key” operation.

Therefore, I ultimately removed the request method condition, kept only the HOST condition, and then used two internal branches for article detail pages and public list pages.

6. EdgeOne Article Detail Page Branch

The article detail page continues to use the previously verified regex:

Plaintext
^/[0-9]{4}/[0-9]{2}/[0-9]{2}/[0-9]+/$

For example:

Plaintext
/2026/07/27/20445/

This branch performs two operations:

  • Custom Cache Key → Query String → Ignore All
  • Origin Request Parameter Setting → Query String → Ignore All
Figure 1: EdgeOne article detail page branch, ignoring query strings in both the cache key and origin requests simultaneously.
Figure 1: EdgeOne article detail page branch, ignoring query strings in both the cache key and origin requests simultaneously.

Both operations need to be configured here.

Ignoring only the Cache Key prevents different parameters from generating different cache objects; however, on a cache miss, the query parameters could still be passed to the origin.

Conversely, removing only the origin parameters cannot guarantee that different parameter variants will reuse the same cache object.

Only by combining both can you simultaneously achieve:

Plaintext
Parameter variants share cache
+
Origin receives only clean URLs

7. EdgeOne Public List Page Branch

Public list pages use a ELSE IF branch at the same level as the article detail page.

The final regex used is:

Plaintext
^(?:/|/page/[1-9][0-9]*/|/category/(?:[^/]+/)+(?:page/[1-9][0-9]*/)?|/(?:tag|series|series-category)/[^/]+/(?:page/[1-9][0-9]*/)?|/[0-9]{4}(?:/[0-9]{2}){0,2}/(?:page/[1-9][0-9]*/)?)$

This regex covers:

  • Homepage and homepage pagination
  • Single-level or multi-level categories and their pagination
  • Tags and their pagination
  • Series and their pagination
  • Series categories and their pagination
  • Yearly, monthly, and daily archives and their pagination

It does not match article detail pages:

Plaintext
/2026/07/30/21863/

The list page branch also sets the following conditions:

Plaintext
Query parameter s: does not exist
Query parameter preview: does not exist
Query parameter preview_id: does not exist
Query parameter preview_nonce: does not exist

Additionally, a special endpoint exclusion condition is retained:

Plaintext
/(?:feed(?:/(?:rss2|rss|atom|rdf))?|embed|trackback)/?$

The operator is “regex does not match”.

This prevents feed, embed, or trackback within multi-level category paths from being treated as standard category hierarchies.

For example, the following URLs will not enter the public list page parameter-removal branch:

Plaintext
/category/program-development/feed/
/category/program-development/embed/
/category/program-development/trackback/

The list page branch also performs:

  • Custom Cache Key → Query String → Ignore All
  • Origin Request Parameter Setting → Query String → Ignore All
Figure 2: EdgeOne public list page branch, including list page regex, search and preview parameter protection, and Feed, Embed, Trackback exclusion conditions.
Figure 2: EdgeOne public list page branch, including list page regex, search and preview parameter protection, and Feed, Embed, Trackback exclusion conditions.

After saving and publishing, the entire rule consists of the article detail page branch and the public list page branch.

Figure 3: The complete structure of the EdgeOne rule after saving. Article detail pages and public list pages share the same rule.
Figure 3: The complete structure of the EdgeOne rule after saving. Article detail pages and public list pages share the same rule.

8. EdgeOne Test Results

After publishing the rule, I sent two requests each to 14 Chinese site list pages:

  1. The first request to the clean URL;
  2. The second request to the same URL, but with a unique test parameter appended.

For example:

Plaintext
https://www.shuijingwanwq.com/page/4/
https://www.shuijingwanwq.com/page/4/?swq_qs_test=random_value

The results for the homepage and homepage pagination both showed:

Plaintext
Clean URL: MISS
URL with parameter: HIT
Body hash identical

The TTFB for the first cold origin fetch of the homepage reached about 14.21 seconds, and /page/4/ reached about 14.68 seconds.

After the parameterized request reused the cache, the TTFB dropped to about 1.2 seconds.

This indicates that once a cache is established for a given list page, subsequent requests—even with random query parameters—do not need to re-execute a WordPress cold origin fetch taking nearly 15 seconds.

Category pages, category pagination, tag pages, and tag pagination also successfully showed:

Plaintext
MISS → HIT

And the body content was identical.

The following pages were also covered by the rule:

  • series-category
  • Series homepage
  • Series pagination
  • Yearly archive
  • Monthly archive
  • Daily archive
  • Various archive pagination

Monthly archive showed two MISSes

During testing, the following two pages showed MISS → MISS:

Plaintext
/2026/07/
/2026/07/page/3/

However, the origin logs showed that both origin fetches received clean URLs without query parameters:

Plaintext
GET /2026/07/ HTTP/1.1
GET /2026/07/ HTTP/1.1

GET /2026/07/page/3/ HTTP/1.1
GET /2026/07/page/3/ HTTP/1.1

The origin did not receive:

Plaintext
?swq_qs_test=...

The two requests came from different EdgeOne IPs, suggesting they were likely routed to different edge nodes rather than the Query String rule failing.

9. EdgeOne Search Page Protection Verification

The search homepage still returns:

Plaintext
You are searching for autopoly - Yongye

Search pagination still returns:

Plaintext
You are searching for autopoly - Page 2 of 2 - Yongye

The content of the search page also differs from the standard homepage and standard pagination.

In the origin logs, the test parameter appeared only in the two search requests:

Plaintext
/?s=autopoly&swq_qs_test=...
/page/2/?s=autopoly&swq_qs_test=...

The other 14 sets of article or list page requests did not pass the test parameter to the origin.

This proves that:

  • Standard list pages ignore the Query String in the cache key;
  • On a cache miss, standard list pages do not pass the Query String to the origin;
  • Search requests are correctly excluded because the s parameter is present;
  • The search homepage and search pagination are unaffected.

10. Cloudflare English Site Configuration

The English site en.shuijingwanwq.com uses Cloudflare.

Here, instead of creating multiple new URL Rewrite Rules, I extended the existing article detail page Query String rule.

The rule name was changed to:

Plaintext
English Article and Public List Page Query String Normalization

The final expression is as follows:

Plaintext
(
  http.host eq "en.shuijingwanwq.com"
  and http.request.uri.query ne ""
  and (
    (
      starts_with(http.request.uri.path, "/20")
      and substring(http.request.uri.path, 5, 6) eq "/"
      and substring(http.request.uri.path, 8, 9) eq "/"
      and substring(http.request.uri.path, 11, 12) eq "/"
      and ends_with(http.request.uri.path, "/")
      and substring(http.request.uri.path, 12, -1) ne ""
      and not (substring(http.request.uri.path, 12, -1) contains "/")
    )
    or
    (
      not any(http.request.uri.args.names[*] == "s")
      and not any(http.request.uri.args.names[*] == "preview")
      and not any(http.request.uri.args.names[*] == "preview_id")
      and not any(http.request.uri.args.names[*] == "preview_nonce")
      and (
        http.request.uri.path eq "/"
        or (
          starts_with(http.request.uri.path, "/page/")
          and ends_with(http.request.uri.path, "/")
          and substring(http.request.uri.path, 6, -1) ne ""
          and not (substring(http.request.uri.path, 6, -1) contains "/")
        )
        or (
          (
            starts_with(http.request.uri.path, "/category/")
            or starts_with(http.request.uri.path, "/tag/")
            or starts_with(http.request.uri.path, "/series/")
            or starts_with(http.request.uri.path, "/series-category/")
          )
          and ends_with(http.request.uri.path, "/")
          and not (
            ends_with(http.request.uri.path, "/feed/")
            or ends_with(http.request.uri.path, "/feed/rss2/")
            or ends_with(http.request.uri.path, "/feed/rss/")
            or ends_with(http.request.uri.path, "/feed/atom/")
            or ends_with(http.request.uri.path, "/feed/rdf/")
            or ends_with(http.request.uri.path, "/embed/")
            or ends_with(http.request.uri.path, "/trackback/")
          )
        )
        or (
          starts_with(http.request.uri.path, "/20")
          and substring(http.request.uri.path, 5, 6) eq "/"
          and (
            len(http.request.uri.path) eq 6
            or (
              substring(http.request.uri.path, 5, 11) eq "/page/"
              and ends_with(http.request.uri.path, "/")
              and substring(http.request.uri.path, 11, -1) ne ""
              and not (substring(http.request.uri.path, 11, -1) contains "/")
            )
            or (
              substring(http.request.uri.path, 8, 9) eq "/"
              and (
                len(http.request.uri.path) eq 9
                or (
                  substring(http.request.uri.path, 8, 14) eq "/page/"
                  and ends_with(http.request.uri.path, "/")
                  and substring(http.request.uri.path, 14, -1) ne ""
                  and not (substring(http.request.uri.path, 14, -1) contains "/")
                )
                or (
                  substring(http.request.uri.path, 11, 12) eq "/"
                  and (
                    len(http.request.uri.path) eq 12
                    or (
                      substring(http.request.uri.path, 11, 17) eq "/page/"
                      and ends_with(http.request.uri.path, "/")
                      and substring(http.request.uri.path, 17, -1) ne ""
                      and not (substring(http.request.uri.path, 17, -1) contains "/")
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )
)

This expression covers:

  • Article detail pages
  • Homepage and homepage pagination
  • Category pages and their pagination
  • Tag pages and their pagination
  • Series pages and their pagination
  • Series categories and their pagination
  • Yearly, monthly, and daily archives and their pagination

Future pagination URLs like the following will also match:

Plaintext
/series-category/example/page/2/
/2026/07/30/page/2/

Cloudflare Rewrite Action

The action remains:

Plaintext
Path: Preserve

Query:
Rewrite to
Static
Value left empty

This preserves the original URL path and only rewrites the Query String to empty.

The rule is placed in the first position of the URL Rewrite Rules.

I initially had some doubts about placing it “first”, but subsequent testing proved that this order caused no conflicts and was not re-appended with query parameters by later rules.

Figure 4: Cloudflare URL Rewrite Rule is active and placed in the first position of the current rule list.
Figure 4: Cloudflare URL Rewrite Rule is active and placed in the first position of the current rule list.

11. Cloudflare Test Results

The English site testing covered:

  • Article detail pages
  • Homepage
  • Homepage pagination
  • Category pages
  • Category pagination
  • Tag pages
  • Series pagination
  • Yearly archive
  • Monthly archive
  • Daily archive

The homepage, homepage pagination, category pages, and tag pages all showed:

Plaintext
HIT → HIT

And the Age for both requests increased continuously.

For example:

Plaintext
Homepage Age: 5319 → 5329
Homepage pagination Age: 38470 → 38474
Category page Age: 38723 → 38728
Tag page Age: 38210 → 38214

This indicates that URLs with random parameters did not create new independent cache objects but reused the existing cache.

Category pagination showed a more direct result:

Plaintext
Clean URL: MISS
URL with parameter: HIT
URL with parameter request Age: 4

Daily archives also showed:

Plaintext
MISS → HIT

Different edge nodes causing two MISSes

Article detail pages, series pagination, and yearly archives previously showed:

Plaintext
MISS → MISS

However, the CF-Ray for the two requests fell on LAS and LAX respectively, indicating the requests hit different Cloudflare edge nodes.

The origin logs ultimately confirmed that even when two origin fetches occurred for these pages, the URLs Cloudflare passed to Nginx were still clean:

Plaintext
/2026/07/13/19426/
/category/blog-operations-en/page/2/
/series/wp-blog-multilingual-guide/page/3/
/2026/
/2026/07/
/2026/07/13/

No standard article or list requests carried the test parameter.

12. Cloudflare Search Page Protection Verification

The English search homepage still returns:

Plaintext
You are searching for wordpress - Yongye

Search pagination still returns:

Plaintext
You are searching for wordpress - Page 2 of 23 - Yongye

The content of the search page also differs from the standard homepage and standard pagination.

Throughout the origin logs, the test parameter appeared only in the two English search requests:

Plaintext
/?s=wordpress&swq_cf_qs_test=...
/page/2/?s=wordpress&swq_cf_qs_test=...

This proves the English site also achieved:

  • Query String removal for standard article and list pages;
  • Search requests retaining the s parameter;
  • Search homepage and search pagination remaining unaffected.

13. Why HTML Hashes Sometimes Differ in Cloudflare Tests

In the EdgeOne Chinese site tests, the full body hash of the clean URL and the parameterized URL were basically identical.

On the Cloudflare English site, however, many pages showed:

Plaintext
same_body: no

Even if both requests were HIT from the same node, the full HTML hash could still differ.

This does not directly indicate a failure of cache normalization.

WordPress pages may contain fields that change with every response, or dynamic content injected by edge nodes or other components. Therefore, the SHA-256 of the full HTML file can only serve as an auxiliary reference.

The success of this normalization was judged primarily based on three pieces of evidence:

  1. Whether CF-Cache-Status changed from MISS to HIT;
  2. Whether Age on the same node increased continuously;
  3. Whether the Nginx origin logs still showed the test Query String.

The final origin logs proved that query parameters for standard article and list pages were indeed removed before the origin fetch.

14. Final Results

After completing the adjustments, both the Chinese and English sites implemented the same logic.

Chinese site EdgeOne:

Plaintext
Article detail pages and public list pages
→ Query parameters do not participate in Cache Key
→ Query parameters are not passed to the origin on cache MISS
→ Search parameter s retained

English site Cloudflare:

Plaintext
Article detail pages and public list pages
→ Query String rewritten to empty
→ Different parameter variants reuse canonical URL cache
→ On cache MISS, origin receives only clean paths
→ Search parameter s retained

Going forward, when the following requests appear:

Plaintext
?amp=1
?nonamp=1
?query-5-page=...
?query-62-page=...
?random=value

As long as they fall within the scope of article detail pages or supported public list pages, they will no longer continuously create independent cache objects and repeated origin fetches due to different parameters.

15. Limitations of This Optimization

Query String normalization can significantly reduce repeated origin fetches, but it cannot solve the underlying issue of a single cold origin fetch being too slow.

In this test, the cold origin fetch TTFB for the Chinese homepage and /page/4/ remained close to 15 seconds, indicating that the dynamic generation speed of WordPress list pages still needs continued observation.

However, this adjustment at least resolved a very important issue:

The same time-consuming page will no longer be repeatedly executed as dozens of different pages due to dozens of meaningless query parameters.

For a server with only a 1-core CPU and pm.max_children = 7, reducing unnecessary PHP requests is more practical than simply increasing the number of PHP-FPM processes.

Next, I will continue to monitor the following metrics:

  • Number of EdgeOne 524 errors
  • Number of Nginx 499 errors
  • PHP-FPM max_children alerts
  • Cold origin fetch latency for homepage and list pages
  • EdgeOne and Cloudflare cache hit rates

Based on subsequent data, I will then determine whether further optimization of WordPress list page queries, W3 Total Cache, Redis object cache, or origin page generation speed is necessary.

系列导航

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

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

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

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

关于我  ·  GitHub  ·  邮件联系