排程按鈕按下去,系統跳出「將於某月某日某時發布」的確認,那一刻你以為這篇文章已經穩了。等到約定的時間真的到,回頭一看,文章列表裡的狀態沒有變成已發布,反而卡在一個更刺眼的字樣——錯過排程。重新手動點一次發布,文章倒是立刻上線,但下一篇排程,說不定又是同樣的結果。
這個狀況會發生在完全沒動過主機的網站,也會發生在剛把整個網站搬到新主機沒幾天的網站,而且兩者的病根完全不一樣:前者多半是流量或快取層擋住了觸發點,後者常是搬家時一行設定跟著檔案搬過去,卻沒人記得在新主機重新接上真正會跑的排程。WordPress 排程失敗看起來只是一個小小的紅字警示,拖著不查,新內容準時上線這件事就永遠只能碰運氣。
先從排程機制本身怎麼運作講起,搞懂它為什麼不是「時間到了自動執行」。

發布時間到了卻沒上線,後台顯示已錯過排程
文章列表裡,原本該顯示已發布的欄位,逾期沒被執行的排程文章通常會被標成一個模糊字眼,不是明顯的失敗訊息,是接近「排程中」但時間已經過去的曖昧狀態,仔細看時間戳記才會發現它比現在早了好幾個小時甚至好幾天。這通常是讀者第一次注意到問題的地方,也是最容易被忽略的地方,因為畫面本身沒有跳出紅色警告,只是安安靜靜地停滯不動。
除了文章列表,後台「工具」裡的「網站健康」(Site Health)也會反映同一件事,而且講得更明白。WordPress 核心內建的排程事件檢測有 3 種文字:一切正常時顯示英文原文「Scheduled events are running」(排程事件運作中);有排程任務執行失敗時顯示「A scheduled event has failed」(有一個排程事件執行失敗),說明文字是該排程事件執行失敗,網站仍可運作,但排程發文或自動更新可能無法如預期進行;有排程任務逾期未執行時則顯示「A scheduled event is late」(有一個排程事件逾期),說明文字同樣提醒排程發文或自動更新可能無法如預期進行。這 3 種狀態不是隨機出現的警語,而是核心程式碼裡明確寫死的判斷結果,看到哪一種,等於已經先幫你分類好問題的嚴重程度。
這個問題不會波及網站前台既有的內容,舊文章照樣能被讀者打開,網站不會因此當機或顯示錯誤頁。它動搖的只有新內容準時上線這一件事:可能是一篇排定好時間對外公告的文章,也可能是外掛與佈景主題的自動更新排程。而這正是它容易被拖著不管的原因:網站表面看起來一切正常,直到排程累積到第二篇、第三篇還是沒上線,才會真的意識到這不是巧合,背後有一個固定的原因,可能發生在完全沒搬過家的網站,也可能是搬家後才冒出來的新毛病,2 種原因完全不同。
WordPress 排程仰賴訪客連線觸發,不是獨立行程
多數人聽到「WordPress 有排程功能」,會直覺以為背後有一支獨立跑著的程式,時間到了自己醒過來執行任務,就像作業系統裡那種真正的系統排程一樣。WP-Cron 這個名字也容易加深這個誤會,它確實叫「cron」,但跟 Unix、Linux 系統裡那種獨立行程完全是兩回事。它是 WordPress 核心自己模擬出來的排程系統,平常什麼都不做,只在有人真的打開網站的某個頁面時才會被喚醒,去檢查資料庫裡有沒有已經到期、該執行的任務。發文排程、外掛與佈景主題的更新檢查、不少外掛自己的定期任務,全部走的是同一套機制,沒有第二套後備方案。
這個設計有一個明顯的取捨,因為完全依附在頁面請求上,「準時」這件事從一開始就無法保證,沒有訪客連線,就沒有這次檢查,任務自然不會準時執行。但反過來看,只要之後任何一次連線觸發了檢查,逾期的任務會被放進佇列裡補做,所以「最終會執行」相對有保障,這跟真正的系統排程時間一到沒執行就直接跳過、永遠不會回頭補,剛好相反。搞懂這個靠連線觸發、事後補做的邏輯,才看得出接下來不管是哪一種成因,本質上都只是在講這次連線出了什麼問題而已。

頁面被載入才會檢查逾期未執行的排程任務
具體來說,這個檢查發生在每一次頁面載入的尾聲。WordPress 官方外掛手冊講得很直接,WP-Cron 的運作方式是在每次頁面載入時,檢查一份待執行任務清單,看有哪些到期該執行,到期的任務會在該次頁面載入時被呼叫執行。換句話說,「頁面載入」是唯一的檢查時機點,沒有第二種觸發方式存在,也沒有背景常駐的程式在旁邊盯著時鐘。
wp_schedule_event() 這個核心函式的官方文件也印證了同一件事,這個動作會在有訪客造訪你的 WordPress 網站、且排定時間已過去時被觸發。這句話點出了「錯過排程」之所以會發生的第一層根因,沒有頁面載入,就沒有這次檢查,排定的任務只能乾等,等到下一次真的有人或有東西連上這個網站為止。
靠非阻塞的迴圈請求呼叫自己的 wp-cron.php
檢查完發現有到期任務之後,WordPress 接下來要做的是「怎麼執行」。它的做法是讓伺服器對自己發出一個 HTTP 請求,打到自己網站根目錄下的 wp-cron.php,而且刻意設計成非阻塞,正在瀏覽頁面的訪客不會因此被拖慢或卡頓,畫面照樣秒開。
不過它終究是一次真實的連線。WordPress 官方進階管理手冊把這種迴圈請求定義為「迴圈請求是指你自己的伺服器或網站嘗試連線回自己」,用途明確包含觸發排程文章與其他排定的事件;同一份文件也提醒,如果看到排程文章或其他定時事件沒有執行,或是網站健康出現迴圈請求失敗的警示,就代表這個環節需要排查。也就是說,只要這次自己打自己的請求在抵達 WordPress 的 PHP 執行環境之前,被別的東西擋下或直接回應掉,這個迴圈請求根本不會真正發生,排程也就無從被喚醒。
沒動過主機的網站,一樣可能卡在流量與快取兩關
確認排程機制的運作邏輯之後,第一大類成因是網站完全沒有搬過家、主機也沒換過,純粹是前面講的觸發機制在這個網站上失靈。這類成因可以拆成 2 種性質不同的斷點:一種是連線本身就不存在,另一種是連線有發生,卻在半路被攔了下來。這兩者彼此獨立,也可能同時出現在同一個網站上,排查時建議都檢查一遍,別找到其中一個原因就急著收工。
低流量時段沒人連線讓排程等不到觸發點
最直觀的一種情況,是排程時間剛好設在半夜或離峰時段,那段時間網站幾乎沒有真人訪客,搜尋引擎的爬蟲則多半不會觸發這個機制,就算會,頻率也遠低於真人流量。時間到了,卻沒有任何一次頁面載入發生,那一次的檢查自然沒被執行,任務只能繼續留在佇列裡等。
這正好對應官方外掛手冊裡舉的那個例子,排定下午 2 點執行的任務,如果一直到下午 5 點才有頁面載入,排程就會晚了整整 3 個小時才被喚醒。等到隔天早上第一個訪客連進網站,任務才會被補做,時間點早就超過原本設定的發布時間一大截。WordPress 官方沒有給出多少流量以上才安全的具體門檻,這也代表流量高低只是機率問題,流量愈低,卡在空窗期的機率愈高,但沒有一個絕對的安全值可以套用。
快取或 CDN 在邊緣直接回應,PHP 根本沒被喚醒
很多排查文章講到「流量太低」就結束,但實際情況更微妙:就算網站流量正常、每天都有真人訪客,只要頁面被快取或 CDN 攔在最前面直接回應訪客,那次連線根本沒有進到 WordPress 的 PHP 執行環境,效果跟完全沒人連線一模一樣。有訪客跟有頁面載入到 PHP 是兩件事,流量正常的網站一樣可能中招,差別只在於外表看起來更難懷疑到這個原因。
以 Cloudflare 為例可以看清楚這個攔截層實際怎麼運作,Cloudflare 是全球廣泛使用的 CDN 與快取服務,這裡只是拿它的官方技術定義示範攔截機制的原理,不是特定推薦。Cloudflare 官方文件把快取命中的 HIT 狀態定義為該資源已存在於 Cloudflare 的快取中,意即這類回應完全由邊緣節點處理,不會被轉送到來源伺服器,也就不會執行來源伺服器上的 WordPress PHP。另一份官方文件也說明,Cloudflare 預設並不會快取 HTML 或 JSON,一般 WordPress 網頁在預設設定下並不會被整頁快取在邊緣;但只要站方另外設定了「Eligible for cache」這類規則,等同舊版 Page Rules 裡的「Cache Everything」,把 HTML 頁面也納入快取範圍,一般網頁請求就會在邊緣被直接回應掉,不再抵達 WordPress。這也解釋了為什麼有些網站裝了 CDN 就開始漏排程,有些裝了卻完全沒事,差別就在有沒有另外把 HTML 也設成整頁快取。本機安裝的快取外掛也可能造成同樣的攔截,原理相同,只要有一層搶在 WordPress 核心接手之前就把回應送出去,那次連線就不會觸發排程檢查。
搬過主機的網站,常卡在這一行殘留的設定
如果前面 2 種都排除了,網站又剛好是最近才換過主機、搬過家,接下來這一段很可能就是 WordPress 排程失敗的答案。舊主機如果用的是關掉 WP-Cron、改用主機端真正的系統排程這種做法,wp-config.php 裡就會有這一行:define( 'DISABLE_WP_CRON', true );。搬家的時候,wp-config.php 整份檔案通常會被原樣搬到新主機,不管是用備份還原工具,還是手動複製檔案,這一行常數也就跟著搬過去、繼續生效。問題是,主機端的系統排程不是寫在這個檔案裡的東西,而是舊主機那台伺服器帳號底下獨立設定的排程項目,這個項目不會被檔案搬遷或資料庫搬遷帶走。新主機上如果沒有人另外重新設定一次,它就是空的。虛擬排程被這行常數關掉了,真正的系統排程又沒有在新主機重建,兩邊都沒人在跑,排程任務永遠不會被執行。
這個斷點很不容易被發現。網站表面上一切正常,前台瀏覽、後台編輯都沒問題,只有排程功能默默失效,往往要等排程累積好幾篇都沒發布,或者去看網站健康的警示,才會意識到有東西壞了。它跟前一節講的快取攔截也有明顯區別:快取攔截通常時好時壞,會因為快取有沒有命中、有沒有剛清過快取而每次結果不一樣;這個殘留設定則是穩定、每一次都會失敗的斷點,因為觸發管道整個被關掉了,不是偶爾被擋,只要沒人動手補上系統排程,狀況不會自己好轉。
這行設定跟著檔案搬走,系統排程卻留在舊主機
DISABLE_WP_CRON 是寫在 wp-config.php 裡的一個 PHP 常數定義,作用是讓 WordPress 在每次頁面載入時,跳過原本會做的檢查逾期任務、發出迴圈請求那整套動作。真正接住排程執行工作的,是舊主機在作業系統或控制面板層級設定的一條排程指令,例如 crontab 裡的一行,或主機商控制面板裡「Cron Jobs」設定的一筆項目,這條指令會定期呼叫網站的 wp-cron.php。這兩者分開存放、分開管理,前者隨網站檔案走,後者留在主機帳號本身,兩者的搬遷方式完全是兩回事。
國際主機商 WP Engine 官方支援文件描述了同一種做法的實際運作方式,其「Alternate Cron」功能運作前提是 wp_cron 必須被設為 false,也就是 WordPress 預設的排程必須先被停用,並且該功能會在網站的 wp-config.php 寫入 define( 'DISABLE_WP_CRON', true );,還會每天透過伺服器端流程確認這個常數持續存在。換句話說,這一行常數在原本的主機環境裡,是搭配一個持續存在的排程機制一起運作的組合式設定,兩者缺一不可。國際主機平台 Pantheon 的官方文件也說明其平台預設會關閉 WP-Cron、改由平台自己的排程系統代為執行,並提醒停用 WP-Cron 之後,必須有服務定期呼叫網站網址,同樣佐證關掉常數與另外接上真正排程,是必須成對出現的 2 個步驟,缺一邊就會是這裡講的斷點。
新主機沒建立對應排程,虛擬排程永遠等不到喚醒
把視角轉到新主機這一端,搬家當下最容易被忽略的一步,就是舊主機控制面板裡設定的那條 Cron Jobs 項目,不會隨著網站檔案或資料庫的搬遷自動出現在新主機上。如果搬家流程只處理了檔案與資料庫,沒有回頭比對舊主機控制面板裡的排程設定、在新主機重新設定一次同樣的項目,新主機從第一天開始就是「虛擬排程被關、真實排程不存在」的雙重空窗。
更麻煩的是,這個空窗不會有任何錯誤訊息主動告知。對 WordPress 來說,DISABLE_WP_CRON 是設定好的,它不會知道外面本來應該有一條排程指令在呼叫它,於是就這樣安靜地停滯著,直到有人主動去查網站健康,或是排程累積到讓人無法忽視,才會被發現。這也是為什麼這類問題常常拖上好一段時間才被抓出來,它不吵不鬧,只是安靜地不執行。
打開設定檔或安裝 WP-Crontrol 確認這個常數是否還在
要確認自己遇到的是不是這個原因,有 2 條路徑可以走,都不難操作。第一條,直接用 FTP 或主機控制面板的檔案總管打開 wp-config.php,用文字搜尋找 DISABLE_WP_CRON 這幾個字,看它有沒有被設成 true。第二條,安裝並啟用 WP-Crontrol 這款收錄在 WordPress.org 官方外掛目錄的免費工具,它會直接列出目前所有排程事件,不用自己翻檔案也能確認狀況。
WP-Crontrol 的官方頁面描述得很清楚,從後台畫面可以查看所有排定的排程事件,包含參數、排程週期、回呼函式與下次執行時間,而且會警示有沒有事件缺少對應動作,或已經錯過排程。2 條路徑對照著看,一條確認常數本身有沒有被關掉,一條確認虛擬排程實際上是不是完全靜止不動,兩邊結果一致,就能確定問題出在哪一層。
搬家補系統排程,快取問題排除 wp-cron.php
找出成因之後,根治的做法分成 2 條路,分別對應前面 2 大類問題:搬家造成的斷點,要在新主機重新接上真正的系統排程;一般成因裡的快取攔截,則要從快取或 CDN 的設定下手,把 wp-cron.php 從整頁快取的範圍裡排除出去。這 2 條路徑不衝突,也不必都做,依自己實際確認出來的成因選對應的做法就好。
不管走哪一條,動手前都要先確認 DISABLE_WP_CRON 目前的實際狀態,它現在是被設成 true,還是根本沒被設定過。這一步不能跳過,因為前面已經講過,這個常數只是關掉虛擬觸發的其中一半,另一半是真正的排程要先接上。順序顛倒過來,在還沒建好替代排程之前就先關掉虛擬排程,會讓排程功能整個停擺,比原本的問題更糟。

在新主機建立對應的系統排程並接上 wp-cron.php
多數主機控制面板都有 Cron Jobs 或類似名稱的設定區塊,做法是新增一筆項目,指令內容用 curl 或 wget 呼叫 https://你的網址/wp-cron.php?doing_wp_cron,結尾不要自己另外帶其他參數值。執行頻率建議設在每 5 到 15 分鐘一次,如果主機提供 WP-CLI,改用 WP-CLI 內建的 cron 指令直接執行到期事件效率更高,因為它只會執行真正到期的項目,不會像瀏覽器方式那樣多打一次不必要的請求。設定完成之後,wp-config.php 裡的 DISABLE_WP_CRON 才維持設為 true,讓虛擬觸發保持關閉,改由這條新排程接手。
至於頻率該抓多密,可以參考國際主機平台的實際做法:WP Engine 官方文件描述其「Alternate Cron」是透過每分鐘對 wp-cron.php 發出請求,檢查有沒有現在該執行的排程;Pantheon 官方文件則說明其平台排程是以每小時一次的頻率,或透過命令列工具隨選執行 WordPress 的排程任務。一個取每分鐘、一個取每小時,說明頻率本來就會因排程任務的急迫程度而不同,需要準時發文的網站可以抓緊一點,只跑外掛更新檢查這類不急的任務,拉長間隔也沒關係,不必套用單一絕對數字。
快取或 CDN 排除 wp-cron.php,不讓它被擋在邊緣
如果確認站方真的有設定整頁快取或「Cache Everything」這類規則,接下來要做的是另外新增一條例外規則,把 wp-cron.php 這個路徑排除在快取或整頁攔截的範圍之外,確保對它的請求一定會被放行到來源伺服器,進到 WordPress 的 PHP 執行環境。本機安裝的快取外掛通常也有「排除特定網址」的設定欄位,做法邏輯相同,只是介面位置不一樣。
這個做法延續的是前面「一般成因」那一節已經引用的 Cloudflare 官方文件:一份說明預設不快取 HTML,只有另外設定規則才會把 HTML 也納入快取範圍;另一份說明 HIT 狀態代表完全由邊緣節點回應、不會抵達來源伺服器。這兩份文件已經足以支撐一個結論,只要把 wp-cron.php 排除在快取規則之外,就能確保它每次都被轉送到來源伺服器執行,不再被邊緣節點攔截下來直接回應掉。
確認排程恢復正常需要三個交叉驗證的動作
設定改完不代表排程真的修好了,改完就假設沒事,是排查裡最容易漏掉的一步。收束整個排查流程的做法,是做 3 個層面不同的驗證:一個看後台內建工具怎麼判斷,一個手動觸發測試機制本身,一個排一篇真正的測試文章驗證系統排程準不準時,3 個都通過,才算真正確認問題解決。
後台 Site Health 面板會列出排程事件的檢測結果
改完設定之後,回到後台「工具」裡的「網站健康」頁面,重新整理查看排程事件那一項的檢測結果。正常狀態應該顯示良好,如果之前顯示有排程事件失敗或有排程事件逾期,這裡會是第一個反映出已經恢復的地方。
這裡對照的正是文章開頭第一節提到的那 3 種狀態文字,正常、失敗、逾期。修改前後把這裡的文字記下來對照一次,比單純憑感覺覺得應該修好了更可靠,因為這 3 種狀態是核心程式碼裡明確寫死的判斷結果,不是憑印象猜測。
手動呼叫 wp-cron.php 觀察排程是否立刻執行
在瀏覽器網址列直接輸入 https://你的網址/wp-cron.php,如果這時候剛好有逾期未發布的排程文章,理論上會立刻被觸發發布。這個動作能拿來確認這支檔案本身能不能被正常執行,也就是前面設定的排除規則或系統排程有沒有在最基本的層面上生效。
要提醒的是,這只是拿來驗證機制本身的手動測試,不是要每次排程都自己手動呼叫一次確保上線。正常情況下,這個環節應該交給前面建好的系統排程自動處理,如果每次都得靠自己手動點一次才會發布,代表根治的設定其實還沒真的生效,只是靠人力補了一次而已。
排一篇測試文章驗證系統排程準時執行的成效
最後一步也是最貼近實際需求的驗證,排一篇測試文章,發布時間設在 10 到 15 分鐘之後,接著什麼都不做,不特意去逛前台衝流量,也不手動呼叫 wp-cron.php,純粹等新設定的系統排程自然跑到那個時間點,看文章有沒有準時變成已發布。
這一步的價值在於它能排除一種常見的誤判,以為自己已經修好了,其實只是不小心用前面 2 個驗證步驟裡的手動觸發,湊巧讓那一篇文章發布成功,系統排程實際上根本還沒真正接上。3 個動作都通過,才代表 WordPress 排程失敗這個問題真的被連根解決,不是碰運氣碰對了一次。
搬過一次家的網站,日後多半不會再犯同一個錯,因為知道了 wp-config.php 裡那一行常數要跟系統排程綁在一起搬。真正麻煩的反而是那些從沒搬過家、卻因為換了一套快取設定或 CDN 規則,讓排程無聲無息失靈的網站,因為沒有人會把加速網站的動作跟排程失敗聯想在一起。
排程失敗留下的訊號其實不少:文章列表的曖昧狀態、網站健康的警示文字、累積不上線的草稿,都是提早浮現的線索。與其等到第三篇、第四篇文章都遲遲沒有發布才回頭查,不如把這篇講的排查順序記下來,先確認機制怎麼運作,再分清楚自己是流量快取的問題還是搬家殘留的設定,最後用交叉驗證的 3 個動作確認真的修好。排程這件事一旦接上真正會跑的系統排程,之後基本上不必再回頭想它,這也是它值得花時間一次處理到位的原因。
