
文章在后台排好了时间,到点却没上线,状态还停在「已排期」,或者多出一行 Missed schedule。这类 WordPress 定时发布故障不报错,日志里也没有红字,很多人先去怀疑主机,换套餐、重装插件,问题照旧。本文按「先确认哪一环没跑」的顺序,教你用 WP-CLI、几条 SQL 和一次 curl 请求把原因缩到一个具体环节,再决定改时区、修 loopback,还是交给系统计划任务。
先确认卡在哪一层:状态、事件、时间戳
排期发布依赖三份数据:文章状态(future 表示已排期)、wp_options 里的 cron 事件(钩子名 publish_future_post)、事件时间戳。三者分开保存,同样的现象可能来自完全不同的原因。后台出现 Missed schedule,说明计划时间已过而状态仍是 future,发布事件没执行或已丢失。
wp post list --post_status=future --fields=ID,post_title,post_date,post_date_gmt
wp cron event list --hook=publish_future_post
wp option get doing_cron
第一条列出仍在排期的文章,把 ID 对照第二条输出里有没有对应事件。事件缺失时改时区、换主机都无效,因为没有东西等着触发;执行 wp post update <ID> --post_status=future 重新排期,WordPress 会按当前时区重建事件。第三条看并发锁,残留未来时间戳会让所有请求跳过调度。

WP-Cron 的触发机制:没有常驻进程,靠访问推动
wp-cron.php 是被页面请求顺路叫起来的:PHP 请求结束前,WordPress 检查有没有到期任务,有的话就向站点自己发起一次非阻塞 HTTP 请求(loopback,服务器请求自身域名的回环访问),由子请求执行任务。
由此带来三条前提:
- 站点要有持续访问,或由系统计划任务定期请求
wp-cron.php;缓存命中率极高、真实 PHP 请求趋近于零时,任务只能排队。 - loopback 必须通。主机防火墙、WAF(Web 应用防火墙,用于拦截异常 HTTP 请求)规则、整站 HTTP 认证都会让请求失败,而前台不报错。
- 并发靠锁串行化。
doing_cron记录带时限的锁(WP_CRON_LOCK_TIMEOUT,默认 60 秒),锁未释放时后续请求跳过调度。
验证只需两步:wp option get doing_cron 看锁,curl -sS -o /dev/null -w '%{http_code}\n' https://example.com/wp-cron.php 看返回码。返回 403、503 或超时而首页正常,基本可判定 loopback 或 WAF 在拦。
时区设置:差 8 小时最常见
WordPress 存两个时间:post_date 是站点本地时间,post_date_gmt 是 UTC,注册事件时用它俩换算时间戳。后台时区若停在 UTC+0,而你按北京时间填 20:00,事件会在次日 04:00 才触发,现象就是晚了 8 小时。
改时区不会修正已排期文章:时间戳创建时就固定了。顺序是先改时区,再让原文重新进入 future,最后用 wp post get <ID> --field=post_date_gmt 核对。
系统时钟漂移更隐蔽:PHP 取操作系统时间,系统慢 7 分钟任务就整体晚 7 分钟。用 date -u 与实际 UTC 对比,差距超 1 分钟就该处理,校准见服务器时间同步配置指南。

主机侧的三类限制
共享型虚拟主机(多个站点共用同一台服务器 CPU、内存与进程配额)上的失败,多数落在三类限制里。
一是 wp-config.php 残留 define('DISABLE_WP_CRON', true);。这行早年常被当作降负载优化写入,迁移脚本会顺手带上。禁用后必须由系统计划任务访问 wp-cron.php,否则永远不跑。用 grep -n "DISABLE_WP_CRON" wp-config.php 确认,删配置或补系统任务,二者必选其一。
二是资源配额把 cron 进程杀掉。计划任务常用来跑备份、生成缩略图、清理修订版本,触及 CPU 或内存上限即被终止,后台只留「跑过但没结果」的印象。WordPress 网站备份策略里的分片错峰思路就是为此;同时留意 PHP 日志的 Maximum execution time。
三是系统计划任务没执行。控制面板里的命令路径、PHP 版本、执行用户任一项不对都会静默失败。顺序是:先确认 cron 服务在跑,再确认命令能手动执行成功,最后才怀疑调度表达式,详见crontab 不执行排查。在共享主机与独立资源间摇摆的,可参考共享主机与独立服务器的取舍,自主配置 cron 的能力正是关键差别。

插件与缓存带来的误判
缓存插件普遍把预加载、清理缓存、生成站点地图挂在 WP-Cron 上,所以 WP-Cron 一卡,同时出问题的还有「缓存不更新」「缩略图不生成」这些看似无关的现象。先停用缓存插件的定时任务再测发布:停用后恢复,问题在任务执行侧;依旧不发,问题在排期事件本身。
对象缓存也会制造假象:doing_cron 锁写进 Redis 或 Memcached 且过期策略不当时会长期有效,表现为排期不执行、手动发布正常。清一次对象缓存再重试,是最低成本的验证。
插件冲突同样会让保存排期失败而不报错,例如两个插件都接管 publish_future_post。定位方法与WordPress 插件冲突导致白屏一致:先全部停用确认恢复,再按「最近安装的优先」逐个启用。
一条可复现的定位顺序
按下面五步走,多数情况十几分钟就能收敛到结论。
- 运行第 1 节的第一条命令,确认状态仍是
future,记下两个时间字段。 - 运行第二条命令确认事件存在并记下 UTC 时间;缺失就重新排期。
- 拿事件 UTC 时间加站点时区反推本地时间,与后台填的对比;对不上即时区或时钟问题。
- 用
curl探测wp-cron.php返回码,再执行wp option get doing_cron;403、503 或残留锁都属触发端阻塞。 - 执行
wp cron event run --due-now:能发出去说明触发端有问题,发不出去说明事件参数或权限有问题。
第 5 步会把到期任务真跑一遍,生产站先确认没有进行中的备份或同步,避免两个进程争抢同一批文件。
修好之后怎么验证
改完配置不等于解决,要用一次真实触发证明。新建一篇测试文章排在 10 分钟后,正文写两句话即可。到点确认三件事:状态是否变成 publish、前台能否打开、wp cron event list 里对应事件是否已清除。
三次测试都准点说明链路已通;仍偶发失败就比对主机资源告警时间与任务执行时间是否重合。测试文章用完删掉。
什么时候该把 WP-Cron 交给系统 cron
判断标准不是站点大小,而是访问是否稳定密集。缓存命中率高的站点真实 PHP 请求很少,loopback 被缓存层和低频访问一起削弱;日访问量个位数的企业站同理,没人在线就没人触发。这类站点更适合用系统 cron 每 5 分钟执行 wp cron event run --due-now,配置见WP Cron 替代为系统 Cron 任务实战。
如果你用的是 Hostease 虚拟主机,控制面板里提供计划任务入口,命令路径与 PHP 版本以面板实际显示为准,拿不准时先与客服确认。需要自行接管系统服务时,VPS(虚拟专用服务器)或独立服务器才给完整的 crontab 与日志权限。
总结一下:定时发布不生效不是单一故障,而是「状态、事件、时间戳、触发、资源」五个环节里某处断了。先用 WP-CLI 确认事件在不在,再核对时区与系统时钟,最后才查主机限制与插件干扰。建议每次改完配置都排一篇 10 分钟后的测试稿验证;如果你需要长期跑大量计划任务,可以考虑把触发方式固定为系统 cron,并单独配日志与告警。