Webhook2026年4月18日閱讀時間 8 分鐘

Stripe Webhook 重試機制:正式環境指南

Stripe 會重試失敗的 Webhook,最長 3 天。重試排程怎麼運作、端點當機時什麼會壞掉,以及該怎麼設計。

嘗試間隔 · 指數成長現在5 分鐘30 分鐘3 小時18 小時放棄status=failed · payload 仍在事件紀錄中 · 仍可重播
三天的重試,然後是一片沉默。這段沉默要不要緊,取決於你自己的紀錄。

你的伺服器當機時,Stripe 送出一個 payment_intent.succeeded,這個事件會怎樣?Stripe 會永遠重試嗎?會重試多久?漏掉的事件又要怎麼對帳?

這是我從整合 Stripe 的開發者那裡最常聽到的問題。它之所以重要,是因為沒處理到的 Webhook,就是產品無法據以行動的營收。客戶剛付了錢。你的履約工作卻從沒觸發。他們的儀表板也從沒更新。

重點摘要

  • Stripe 以指數退避重試 3 天,之後就永遠停止
  • 快速回 200、以非同步方式處理、處理前先記錄每個事件、保持冪等
  • 如果沒時間自己做這一切,Webhook 中繼服務(webhook relay)只要換一個網址,就能給你重試、事件紀錄和重播

Stripe 怎麼重試

Stripe 預期你的 Webhook 端點很快就回傳 2xx 狀態碼。它沒有公布確切的數字,文件只說要在「任何可能導致逾時的複雜邏輯之前」回傳 2xx。社群實際依循的數字是 20 秒。逾時的傳遞算失敗,5xx 回應和網路錯誤也一樣。

失敗時,Stripe 會以指數退避重試這個事件,最長 3 天。重試的間隔越拉越長,最後一段大約落在原始事件的 72 小時後。

3 天後,Stripe 會把事件標為失敗並停止重試。它不會叫醒你,也不會寄備援通知信。這個事件就這樣從你的傳遞佇列裡消失,雖然你還是能在 Stripe Dashboard 的 Developers → Events 看到它。

Stripe 的重試曲線也算溫和。Merchant of Record 平台可能重試得兇得多:Paddle 會在大約兩天內最多嘗試 60 次,一個跟得上 Stripe 排程的處理程式,仍可能被 Paddle 的重試淹沒。

如果失敗持續下去,Stripe 最後會整個停用端點,並寄信告訴你停用日期。從那裡復原是另一件工作,而事件在 30 天內都還能從 Events API 拉回來:回填步驟見「Stripe 停用了你的 Webhook 端點」。

三種會咬人的失敗模式

端點太慢。超過那個未公布的逾時才回應,Stripe 就算失敗。如果你的 Webhook 處理程式同步寫入一個很慢的資料庫、同步到 Salesforce,再寄一封信給客戶,每個事件都在逾時邊緣遊走。

部署空窗。伺服器在部署時重啟,漏掉大約 10 個事件,然後恢復。Stripe 會重試這些事件,但只有 3 天。

bug 造成一連串 500。你推了一次部署,遇到某個特定的事件類型就拋出例外。Stripe 會重試,卻一直撞上同樣的錯誤。3 天後 Stripe 放棄,你就有了一個無聲的資料缺口。

這三種情況裡,Stripe 都做得沒錯。失敗出在管線你這一端。

正式環境等級的整合會做的四件事

1. 快速回 200,以非同步方式處理

不要在 Webhook 處理程式裡做繁重的工作。接下事件,寫進佇列或持久的紀錄,回傳 200,再以非同步方式處理。這能讓你遠離逾時,也讓傳遞和商業邏輯脫鉤。

export async function POST(req: Request) {
  const event = await verifyStripeSignature(req);
  await queue.publish(event);   // durable write
  return new Response(null, { status: 200 });
}
waitUntil · 這個 200 是你守不住的承諾驗證回傳 200waitUntil(work)只在記憶體裡被回收 → 消失,不會重新傳送先寫入 · 這個 200 是真的驗證持久寫入回傳 200處理可重試
只對已經持久寫下的東西回 200,服務商的計時就不再限制你的商業邏輯。

2. 處理前先記錄每個事件

在碰商業邏輯之前,先保存原始 payload 和標頭。邏輯拋出例外時,你可以從紀錄重播。Stripe 3 天後就停止重試,事件也只保存 30 天,過了之後,能讓你復原的就只剩你自己的紀錄。

3. 在你這端做到冪等

Stripe 重試時會帶同一個 event.id,所以處理程式必須冪等:如果你已經處理過 evt_1H...,就不要再處理一次。一個簡單的做法:

INSERT INTO processed_events (stripe_event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (stripe_event_id) DO NOTHING;

如果這次寫入影響了 0 列,代表你之前就看過這個事件。跳過。

一旦重試開始同時抵達,或順序錯亂,冪等性就比看起來深得多。「同一個 Webhook 你收到了兩次」談了交易邊界、針對無法回滾的副作用所用的先認領再提交(claim-then-commit)模式,以及為什麼修正順序錯亂的傳遞要靠版本防護,而不是去重。

4. 失敗時發出警示

如果連續 5 個事件失敗,你會希望在幾分鐘內就知道,而不是等客戶寫信給客服。追蹤連續失敗的次數,並設定值班通知。

AnyHook 的角色

AnyHook 位在你的伺服器前面,在傳輸層解決這個問題。你把 Stripe 指向 in.anyhook.net/you/stripe,而不是你的來源伺服器,接著我們會:

  • 大約一秒就回 200 給 Stripe,所以回應時限永遠不會是問題
  • 記錄每個事件的完整標頭和請求主體,儲存時已加密
  • 依 5 分鐘 → 30 分鐘 → 3 小時 → 18 小時 → 之後每天一次的排程,重試傳遞到你的伺服器:Free 方案 3 次嘗試,Pro 方案 5 次,Scale 方案 10 次。Stripe 自己的排程最長持續三天,所以在 Free 和 Pro 方案上,長期的安全網是儲存下來的事件紀錄和重播,而不是重試次數
  • 提供一鍵重播。如果你的伺服器當機了一小時,在 App(一個接收網址,對應一個 Webhook 來源)頁面按一次,就能重播那一小時內失敗的事件(一次最多 100 個),而且重播失敗的事件不計入每月配額,不過有每日上限,也會用到你的運算額度
  • 連續失敗時發出警示。連續失敗 1 次和 5 次時,我們會寄信通知你。到了 20 次,我們會暫停傳遞到那個目的地(第一次暫停 10 分鐘,再次跳脫會更久),並再寄一次信;暫停期間到達的事件會記錄下來供重播,而不會傳遞

不用 SDK,只換一個網址。第一次傳遞照樣能通過你現有的 Stripe 簽章檢查。重試和重播帶的是 Stripe 原本的時間戳記,Stripe SDK 在五分鐘後就會拒絕,所以要改用 AnyHook-Signature 標頭來接受這些請求。

想在註冊之前先看看,一個指令就能拿到可用的接收網址(inbound URL),不需要帳號:

curl -X POST https://anyhook.net/api/v1/quickstart

回應裡有一個 inbound_url,讓你設定給服務商;一個 api_key,用來透過 API 讀取事件紀錄;還有一個 claim_url。這個端點是暫時的:除非你開啟認領網址並登入,否則端點和它的事件會在 7 天後刪除。認領之後,網址維持不變。

結論

如果你現在正在遺失 Stripe 事件,解法通常不是加大重試,而是處理之前先持久儲存,讓你永遠有東西可以重播。

所有文章2026年4月18日 · 8 分鐘

別再漏掉 Webhook。

換一個網址,就有重試、事件紀錄和一鍵重播。或者先親眼看看:註冊後跑一次 30 秒的當機演練,看著一個事件在伺服器當機時保存下來。不需要伺服器。