In my WordPress + Polylang multilingual site, I have been using a PHP script to synchronize and copy newly added Chinese tags into English tags, and to establish the Chinese-English translation relationships.
This script has been running for a long time, but there has always been a confusing issue:
The total number of Chinese tags displayed in the WordPress admin dashboard occasionally does not match the total number of Chinese tags actually read by the script.
This is not the first time this issue has occurred.
I had previously conducted two complete rounds of troubleshooting around it, which I documented in separate articles:
The first investigation pointed the problem to residual Polylang translation relationship caches in W3 Total Cache; the second investigation found that using dynamic pagination while creating tags during iteration caused the pagination range and statistical results to shift. (永夜)
Both of these issues did indeed exist, and the previous fixes were not ineffective.
But it wasn’t until this time that I confirmed:
The true root cause of the script chronically showing “the admin total is correct, but running the script directly reads fewer tags” is that
get_terms()hit a persistent staleWP_Term_Queryquery result cache.
The previous two investigations resolved peripheral and concurrency issues, but did not thoroughly address the cache reliability when the script reads the source tag list.
1. What This Script Does
My WordPress blog uses Polylang to manage Chinese and English content.
When adding new Chinese articles, some new Chinese tags are usually added as well. To avoid manually creating English tags in the admin dashboard every time, I use a command-line PHP script to automatically accomplish the following:
- Read all Chinese tags;
- Check whether each Chinese tag already has an English translation;
- Create English tags for those lacking translations;
- Set the English language assignment;
- Establish the Polylang Chinese-English translation relationship;
- Output statistics for processed, skipped, and incomplete tags.
The script is very simple to run:
cd /data/wwwroot/www.shuijingwanwq.com
php polylang-batch-zh-to-en-tags.php
Under normal circumstances, the number of Chinese tags read by the script should match the total number of Chinese tags displayed in the WordPress admin dashboard.
But this time, that was clearly not the case.
2. The Fault Reappears: 8928 in the Admin Dashboard, but the Script Consistently Shows Only 8915
The WordPress admin dashboard shows:
中文标签总数:8928
But no matter how many times I ran the synchronization script, the output was always:
源语言标签总数:8915
已处理新标签:0
已跳过已有翻译:8915
The difference between the two is:
8928 - 8915 = 13
What was even more abnormal was that this was not a one-time statistical delay.
I subsequently continued to:
- Add tags to published articles that had none;
- Delete some new tags and re-add them;
- Publish articles with new tags;
- Run the synchronization script again.
As a result, the script still steadily remained at:
8915
This showed that the problem was no longer a simple admin dashboard statistical delay.
The script was getting a fixed, unchanging list of stale tags.
3. Why the Previous Two Investigations Did Not Truly Solve the Problem
Looking back at the previous two rounds of troubleshooting, it becomes clear that they resolved issues on two different levels.
1. The First Investigation Resolved the Polylang Translation Relationship Cache
The first time, after a WordPress upgrade, the script misjudged several untranslated tags as already having English translations.
After clearing W3 Total Cache, the script worked normally again.
Therefore, the conclusion at the time was:
Residual old Polylang translation relationship caches in W3 Total Cache caused
pll_get_term()to return incorrect results.
This judgment was valid in that scenario. The problem did indeed disappear after clearing the cache. (永夜)
But at the time, I did not further confirm:
- Exactly which cache group it was;
- Whether it was the translation relationship cache or the term query cache;
- Why it would recur later;
- How to avoid hitting stale data again at the code level.
In other words, the symptoms were treated, but no long-term protection was established.
2. The Second Investigation Resolved Dynamic Pagination Pollution
During the second investigation, the script produced statistics like this:
源语言标签总数:8328
已处理新标签:0
已跳过已有翻译:9000
处理率:108.07%
It was discovered that the script was reading tags with pagination while simultaneously creating new English tags in the database.
If offset or dynamic pagination queries continued to be used, the result sets for subsequent pages could shift, leading to:
- Certain tags being skipped;
- Certain tags being read repeatedly;
- The number of skipped tags exceeding the actual total source tags;
- The final processing rate exceeding 100%.
The final solution was:
First fetch all Chinese tag IDs at once, and then process them in batches in memory via
array_chunk().
This modification resolved the dynamic dataset pagination issue. (永夜)
However, the get_terms() used to “fetch all Chinese tag IDs at once” still allowed reading from the cache.
This means that although the static list would not change during execution, it could still be a stale list from the very beginning.
4. The Most Misleading Part This Time: WP-CLI Queries Were Actually Normal
To confirm whether it was an issue with get_terms() itself, I first tested both the default query and the cache-disabled query via WP-CLI.
The results were:
默认查询:8928
禁用缓存:8928
差值:0
From these results, it is easy to draw a conclusion:
There is no cache problem, because the results of both queries are exactly the same.
I一度 also据此认为,问题可能在:
- Production files being out of sync with repository files;
- Differences in script loading order;
- Abnormal Polylang language filtering conditions;
- PHP CLI OPcache;
- A MU plugin modifying the query.
But later, following the script’s actual execution method, I compared it in an environment that directly executes the PHP file, and the results were completely different.
5. Key Evidence: In the Same Direct PHP Process, the Default Query Returns 8915, While Disabling Cache Returns 8928
The final diagnosis did not continue to rely on WP-CLI, but was fully reproduced:
php polylang-batch-zh-to-en-tags.php
In this direct PHP CLI execution environment.
Running both within the same execution context:
get_terms([
'taxonomy' => 'post_tag',
'lang' => 'zh',
'hide_empty' => false,
'fields' => 'ids',
]);
And:
get_terms([
'taxonomy' => 'post_tag',
'lang' => 'zh',
'hide_empty' => false,
'fields' => 'ids',
'cache_results' => false,
]);
The results obtained were:
默认 get_terms():8915
cache_results=false:8928
差值:13
At this point, the root cause was very clear:
When executing the PHP script directly, the default
get_terms()hit a persistent stale Term Query cache.
And after setting:
'cache_results' => false,
the query fetched the complete results from the current data state again, correctly returning 8928 Chinese tags.

6. Why WP-CLI and the Direct PHP Script Get Different Results
This is also the part of the whole investigation most prone to contradictions.
The WP-CLI query returns:
8928
But the direct PHP script default query returns:
8915
They both appear to load the same WordPress, but their execution contexts are not entirely the same.
WP-CLI defines and initializes its own runtime environment, for example:
WP_CLI = true
Whereas when directly executing a normal PHP file, the same CLI context does not exist.
On the current site, the following also exist simultaneously:
- W3 Total Cache Object Cache;
- Redis persistent object cache;
- Polylang language filtering;
- Custom MU plugins;
- Multi-domain Host recognition;
- PHP CLI OPcache.
Therefore, different entry points may use different:
- Cache namespaces;
- Host contexts;
- Cache bypass logic;
- Plugin initialization paths;
- Cache key combinations.
This means:
Normal queries in WP-CLI cannot prove that the cache in the normal
php file.phpexecution environment is also normal.
A truly reliable diagnosis must reproduce the original execution method where the problem occurs.
7. The SQL Itself Is Fine; the Problem Occurs at the Query Result Cache Layer
During the diagnosis, the actually generated SQL was also checked.
The query had no abnormal LIMIT, and no incorrect language conditions:
SELECT t.term_id
FROM wp_terms AS t
INNER JOIN wp_term_taxonomy AS tt
ON t.term_id = tt.term_id
INNER JOIN wp_term_relationships AS pll_tr
ON pll_tr.object_id = t.term_id
WHERE tt.taxonomy IN ('post_tag')
AND pll_tr.term_taxonomy_id IN (8837)
ORDER BY t.name ASC
In other words:
- The complete Chinese tags exist in the database;
- Polylang’s Chinese language relationships are correct;
- The SQL query conditions did not miss any new tags;
- Real-time database queries can return 8928 results.
What actually returned 8915 was a previously saved stale query result.
This also explains why the count remained fixed for a long time.
8. The 13 Cached Missing Tags Do Not Contradict the 9 Actually Missing Translation Tags
During the diagnosis, a seemingly contradictory piece of data also appeared:
旧缓存遗漏:13 个中文标签
真正缺少英文翻译:9 个中文标签
These two numbers are not required to be equal.
13 tags did not enter the old Chinese source tag list, but 4 of them had already established English translation relationships through other processes, such as:
- Manual operations in the admin dashboard;
- Automatic synchronization during the article translation process;
- Processing by other plugins or scripts.
Therefore, only 9 actually need English translations to be filled in currently.
These 9 tags are:
代码审查
占位符保护
Token 校验
翻译稳定性
安全加速流量
域名拆分
安全加速请求
加量包
网站成本控制
9. The Final Fix Was Only Two Lines
After confirming the root cause, I did not redesign the script, nor did I do a large-scale refactor of the tag creation process.
I only added a comment and a parameter to the query fetching all Chinese tag IDs:
// 批处理维护脚本必须绕过持久化的 term-query 缓存,确保读取最新完整标签集合。
$source_term_ids = get_terms([
'taxonomy' => 'post_tag',
'lang' => $source_lang,
'hide_empty' => false,
'fields' => 'ids',
'cache_results' => false,
]);
The core change was only:
'cache_results' => false,
The corresponding Git commit is:
f079037925dcf048bf1d455cf2a6badfb830df64
fix: bypass stale term query cache in Polylang tag batch
This fix did not turn off caching for all of WordPress, nor did it affect frontend page performance.
It only dictates:
When this maintenance script reads the set of source tags to be processed, it must not trust the stale query result cache.
For daily page requests, caching can improve performance.
But for maintenance scripts responsible for filling in, migrating, and verifying data, accuracy is more important than reusing query caches.
10. First Run Results After Production Deployment
Before deployment, I backed up the production files, and then checked the syntax of the temporary and production files:
No syntax errors detected in /tmp/polylang-batch-zh-to-en-tags.php.new
No syntax errors detected in /data/wwwroot/www.shuijingwanwq.com/polylang-batch-zh-to-en-tags.php
After deployment, the SHA256 was exactly the same as the repository file:
9b8e1993914571da1bd4f2d6849d6fa318b2572093d86ed5fc686636ce12ac68
Then I reran the script:
php polylang-batch-zh-to-en-tags.php
The script finally read the complete count from the admin dashboard:
源语言标签总数:8928
The final statistics were:
源语言标签总数:8928
已处理新标签:9
已跳过已有翻译:8919
处理率:100.00%
The issue where it was chronically stuck at 8915 has disappeared.
This also proves in reverse from the production run results:
cache_results=falseindeed hit the true point of failure.
11. The Remaining 3 Tags Not Establishing Relationships Is a Separate Issue
After the first complete run finished, the script prompted that 3 tags still had not successfully established English translation relationships:
Token 校验
占位符保护
翻译稳定性
Calling them again in a new PHP process:
pll_get_term($source_id, 'en');
The three tags still returned:
英文 ID:0
Therefore, this was not a false report caused by the in-process cache at the end of the script, but rather a separate issue in the tag creation or translation relationship binding stage.
Since only 3 tags were left, the cost and risk of continuing to rewrite the batch script was higher than handling them manually, so I ultimately supplemented them directly in the WordPress admin dashboard:
Token 校验 → Token Validation
占位符保护 → Placeholder Protection
翻译稳定性 → Translation Stability
The corresponding slugs are:
token-validation
placeholder-protection
translation-stability
This minor anomaly does not affect this root cause diagnosis regarding the “incomplete reading of the total source tags”.
On the contrary, it also reminded me:
“Entering the processing flow” does not equal “successfully establishing the translation relationship in the end”.
Going forward, the processed statistics in the script should ideally only increment after final successful verification, rather than incrementing as soon as an attempt to create a tag begins.
12. Why This Time Counts as a True Resolution
The previous two investigations both yielded a solution that temporarily got the script working normally again:
First time:
清除 W3 Total Cache
Second time:
一次性获取源标签 ID,再通过 array_chunk() 分批处理
They respectively resolved:
- Polylang translation relationship cache residue;
- Result set shifts during dynamic pagination.
But the script’s most critical source data entry point remained:
get_terms()
As long as this query continued to allow reading persistent stale caches, it could still fetch an expired set of Chinese tag IDs again.
This fix wrote the guarantee into the code:
'cache_results' => false,
From now on, regardless of whether stale Term Query results remain in the object cache, this batch script will re-read the current complete set of source tags.
This is a long-term fix that does not rely on human memory, does not require clearing the cache every time, and does not depend on the current cache state.
13. Lessons Learned from This Investigation
1. You Must Reproduce It in the Execution Entry Point Where the Error Actually Occurs
WP-CLI being normal does not mean:
php custom-script.php
is also normal.
Different execution entry points may have different cache and plugin contexts.
2. Clearing the Cache Can Validate the Direction, but It Isn’t Necessarily a Permanent Fix
Returning to normal after clearing W3TC or Redis only proves the problem is cache-related.
You still need to further confirm:
- Which query it is;
- Which cache layer it is;
- Why it did not invalidate correctly;
- Whether it can be actively bypassed in critical scripts.
3. A Static List May Also Be an Expired Static List
Fetching all IDs upfront and then processing them with array_chunk() is indeed more stable than dynamic pagination.
But you also need to ensure that this initial ID list comes from real-time data, not from a stale query cache.
4. Data Maintenance Scripts Should Prioritize Accuracy
Frontend pages need cache performance.
Batch, migration, repair, and verification scripts, however, should prioritize:
完整
实时
可验证
可重复运行
For a script that processes thousands of tags in a single run, the benefit of saving one term query is far outweighed by the long-term data inconsistency caused by missing tags.
5. Statistics Must Represent True Success, Not Attempt Counts
The output in this run:
已处理新标签:9
actually included 3 tags that ultimately failed to establish translation relationships.
A more rigorous statistical breakdown should be:
尝试处理数量
成功创建数量
成功绑定翻译关系数量
失败数量
跳过数量
Otherwise, a superficial 100% processing rate can still mask localized failures.
14. Conclusion
This issue was investigated three times in total.
The first time, I thought it was residual Polylang cache after the WordPress 7.0 upgrade.
The second time, I thought it was the script creating new tags during dynamic pagination, causing the dataset to shift.
Neither of these judgments was wrong, but they were only part of the problem.
The real reason the script chronically read a stale total was:
get_terms()in the direct PHP execution environment hit a persistent stale Term Query result cache.
The final fix was just one parameter:
'cache_results' => false,
But to confirm this single line, the process involved:
- Two troubleshooting articles;
- Multiple rounds of database and cache checks;
- Comparing WP-CLI and direct PHP environments;
- SQL query cross-checking;
- Comparing the difference sets between cached and non-cached results;
- Repository commits and production deployment verification.
This is also a very typical class of problem when maintaining large WordPress multilingual sites:
The hardest things to troubleshoot are not direct errors, but rather when the code executes normally, the output looks reasonable, but the data read was stale from the very beginning.
This time, the issue of the tag synchronization script’s “chronic total mismatch” no longer relies on the luck of clearing the cache, but has been stably resolved at the code level.
需要长期技术维护或远程问题排查?
我是拥有 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


发表回复