重送、順序、冪等性的設計

Webhook 重送 冪等性 順序 重試
本文的對象
適用於實作 Webhook 接收端的開發者。說明在「同一事件收到多次」「舊事件在新事件之後才到」等情況下也能正確運作的接收端做法。

Webhook 是方便的機制,但網路是不穩定的,伺服器是會宕機的。ReceiptRoller 具備「沒送達」時自動重送的機制,但其結果是同一事件送達 2 次以上順序前後顛倒都可能發生。接收端請以此為前提設計。

重送政策

接收端沒有回傳 2xx 的情況下,ReceiptRoller 會以指數退避重試重送。

發送時機
第 1 次即時
第 2 次1 分後
第 3 次5 分後
第 4 次30 分後
第 5 次2 小時後
第 6 次6 小時後
第 7 次24 小時後

合計約 32 小時期間內最多重送 7 次。即使如此仍未成功的情況下,會作為無效信件記錄,可在開發者入口的「Webhook 投放紀錄」確認。

作為重送對象的回應

  • 5xx(伺服器錯誤)
  • 429(速率限制)
  • 逾時(10 秒無回應)
  • 連線錯誤(DNS / TLS / TCP)

不作為重送對象的回應

  • 2xx:視為成功而不重送
  • 3xx:不自動跟隨重新導向(視為失敗但也不重送)
  • 4xx(含 401/410):視為接收端的永續性拒絕而不重送
注意:接收端「處理失敗但希望重試」的情況請回傳 5xx。回傳 4xx 則不會被重送。

順序不被保證

ReceiptRoller Webhook 不保證投放順序。例如可能發生下列這樣的情況。

  • receipt.refundedreceipt.issued 更早到達
  • product.updatedproduct.created 更早到達
  • 第 1 次的投放被重送,舊事件在新事件之後到達

在需要處理順序的業務中,請看酬載中含的 occurred_at(事件發生時刻),設計為在接收端重新排序,或以 API 重新取得最新狀態。將 Webhook 不當作「最新狀態的通知」而當作「有變更的觸發」處理較安全。

冪等性的實作

讓同一事件收到多次結果也不改變,稱為冪等性。ReceiptRoller 為各事件附帶唯一的 event_id(以及 X-RR-Event-Id 標頭),請以此為鍵避免重複處理。

模式 1:記錄已處理 ID

async function handleWebhook(event) {
  // 若已處理則忽略
  const exists = await db.processedEvents.findOne({ event_id: event.event_id });
  if (exists) return;

  // 業務處理
  await processEvent(event);

  // 記錄(與業務處理在同一交易內為理想)
  await db.processedEvents.insert({
    event_id: event.event_id,
    received_at: new Date(),
  });
}

已處理表設定 TTL,自動刪除舊記錄,可防止肥大化(考慮重送期間 32 小時,最低也保持 7 天)。

模式 2:以 UPSERT 寫入

若業務處理是「將某筆記錄設為最新狀態」,設為 UPSERT(INSERT or UPDATE)則自然變冪等。

// 接收庫存事件,以商品 ID 為鍵 UPSERT
await db.inventory.upsert({
  where: { product_id: event.data.product_id },
  update: { quantity: event.data.quantity, updated_at: event.occurred_at },
  create: { product_id: event.data.product_id, quantity: event.data.quantity },
});

在這個模式中,為了在舊事件後到時不覆蓋最新值,需要加入 occurred_at 比較的巧思。

模式 3:以自然鍵判定

若知道業務上的自然鍵(例:receipt_id),以此偵測重複也很簡單。與使用 event_id 的模式併用會更堅固。

順序顛倒的因應

對同一實體的多個事件順序顛倒時的典型對策。

  • 以 occurred_at 進行覆蓋判定:若比接收端持有的最終更新時刻更舊的事件則不套用
  • 版本號:酬載有 version 的情況,僅採用更大的值
  • 狀態轉移檢查:像「refunded → issued」這樣業務上不可能的轉移則忽略
  • API 重新取得:收到通知後以 API 重新取得最新狀態(不用考慮順序就完成)

無效信件的處理

重送 7 次仍未成功的事件會作為「無效信件」保存,可在開發者入口 → 該端點 → 「投放紀錄」分頁確認。各記錄含有下列資訊。

  • 事件 ID、種類、時間戳
  • 各試行的回應碼與主體(開頭 1KB)
  • 「重新投放」按鈕

修正接收端後,可用「重新投放」按鈕手動重送。期間為自投放起 30 天。

常見反模式

容易做錯的實作 問題
因為處理重,投入非同步佇列後不回傳 2xx,等到處理完成 10 秒逾時而進入重送迴圈
業務處理失敗時回傳 200 不會被重送而事件漏失
無冪等性地對庫存加減算 重複投放使庫存數混亂
不考慮順序顛倒就覆蓋最新旗標 舊事件踐踏新狀態
沒監視無效信件 沒察覺故障而經過數天

建議的接收模式

  1. 簽章、時間戳驗證
  2. event_id 進行冪等性檢查(已處理則即 200
  3. 將酬載投入內部佇列(SQS / Cloud Tasks 等)
  4. 即刻回傳 200(到此為止 1 秒以內為目標)
  5. 以佇列 worker 執行業務處理(重試在內部進行)

這個模式的話,接收端點總能高速回應,業務處理的失敗不會連結到重送迴圈。

相關指南

發布日: 2026-04-27 更新日: 2026-07-06