Webhook失敗イベントの再送・再処理手順|重複実行と取りこぼしを防ぐ運用
Webhookは、決済、申込、アカウント変更、CRM更新、AIジョブ完了などを別システムへ届ける仕組みです。障害が起きたときは「同じイベントを再送すれば戻る」と考えがちですが、配信そのものが届いていない場合と、受信後の業務処理だけが失敗した場合では、再実行する場所が異なります。
結論から言うと、Webhookの復旧は、送信側の配信履歴、受信側のイベント台帳、業務処理の結果を同じevent IDで結び、未達だけを再送し、受信済み・処理未完了だけを内部キューから再処理します。どちらの場合も冪等性を先に確認し、少量から再開し、最後に件数と結果を照合します。
本記事のポイント
- 配信失敗と受信後の処理失敗を、送信側delivery ID・event ID・受信時刻・処理状態で切り分けます。
- イベント保存と冪等性判定を業務処理より先に行い、失敗分はデッドレターキューへ隔離して少量から戻します。
- 再処理後は入口の成功ではなく、配信台帳、受信台帳、業務結果、残存失敗件数の一致で完了を判定します。
配信失敗と処理失敗を最初に切り分ける
Webhook障害を一つの「受信失敗」とまとめると、すでに受け取ったイベントを送信サービスから再送し、CRM更新や通知を二重に実行する危険があります。最初に、イベントがどの境界まで到達したかを確認します。入口は、送信元、受信エンドポイント、耐久保存、内部キュー、ワーカー、最終業務システムの順です。
| 状態 | 確認できる証拠 | 再開する場所 | 避ける操作 |
|---|---|---|---|
| 送信側で未配信・非2xx・timeout | delivery ID、event ID、応答コード、試行時刻 | 送信側の手動再送または公式再配信API | 受信済み確認なしの一括再送 |
| 受信したがイベント保存前に失敗 | 入口ログはあるが受信台帳にevent IDがない | 送信側から対象だけ再送 | アクセスログだけを成功扱いする |
| 受信・保存済み、内部処理が失敗 | 受信台帳にevent IDがあり、処理状態が失敗・保留 | 内部キューまたはデッドレターキューから再処理 | 送信側から同じイベントを重ねて再送 |
| 処理済みだが応答だけ失敗 | 業務結果は存在するが送信側は非2xx・timeout | 冪等判定で処理済みに着地させる | 請求・通知・登録を再実行する |
| 結果が不明 | 台帳、ログ、業務結果のいずれかが欠落 | 隔離して個別確認 | 推測で成功・失敗を決める |
送信サービスによって配送仕様は異なります。Stripeの公式資料では、ライブ環境の失敗配送を最長3日間、自動的に指数バックオフで再試行し、管理画面では過去15日、CLIでは過去30日のイベントを手動再送できます。手動再送しても自動再試行は止まりません。一方、GitHubは失敗したWebhook配送を自動再送しないため、配送履歴を確認して画面またはREST APIから再配信します。サービス名だけで「いつか自動で戻る」と判断せず、対象endpointの仕様と現在の配送記録を確認します。
イベントの順序も前提にしません。Stripeはイベント順序を保証しないと明記しています。更新イベントが作成イベントより先に届くような場合は、event IDを順番に流し直すだけでは正しい状態にならないことがあります。イベント本文に含まれる対象IDから最新状態を取得する、対象ごとのversionや発生時刻を比較するなど、業務システム側で順序逆転を吸収します。
Webhookの認証や受信口の基本から見直す場合はWebhookを使った非同期処理基盤、署名secretの交換が原因ならWebhook署名シークレットのローテーション手順を先に確認してください。本記事では、正規イベントの配送または後続処理が失敗した後の復旧に焦点を絞ります。
二重実行を防ぐ記録を処理より先に残す
再送の安全性は、再送操作ではなく通常時の受信設計で決まります。受信したリクエストは、署名と時刻を検証した後、業務処理より先にイベント台帳へ耐久保存します。保存が完了したら短時間で2xxを返し、CRM更新、メール送信、ファイル生成などの重い処理は内部キューへ渡します。GitHubは10秒以内に2xxを返し、非同期処理を使うことを推奨しています。Stripeも複雑なロジックより先に成功応答を返し、非同期キューで処理する方法を案内しています。
| 記録項目 | 用途 | 注意点 |
|---|---|---|
| provider・endpoint・環境 | 送信元と本番・検証の混在を防ぐ | URLだけでなく契約・tenantも識別する |
| delivery ID・event ID | 同じ配送と同じ論理イベントを追跡する | 送信元ごとの一意性範囲を確認する |
| event type・object ID | 同じ対象に対する異なるイベントを区別する | event IDが異なる重複通知にも備える |
| 受信時刻・発生時刻・payload hash | 順序、改変、同一本文を比較する | 個人情報やsecretを通常ログへ複製しない |
| 処理状態・試行回数・次回時刻 | 受付済み、処理中、成功、再試行、隔離を管理する | 無限再試行を許さない |
| 副作用の冪等キー | 請求、CRM更新、通知などの二重作成を止める | 一つのイベントに複数副作用があれば個別管理する |
| 最終結果ID | 作成されたレコードやジョブとイベントを結ぶ | HTTP成功だけで完了にしない |
冪等性は「event IDをログに書く」だけでは成立しません。イベントを処理する前に、providerとevent IDの組み合わせへ一意制約を置き、同じイベントが来たら、未完了なら同じ処理状態を参照し、完了済みなら副作用を再実行せず成功応答へ着地させます。GitHubのX-GitHub-Deliveryはイベントごとに一意で、再配信でも同じ値が使われます。Stripeは処理済みevent IDの記録を案内し、別のEventオブジェクトとして同じ内容が届く重複にはdata object IDとevent typeを併せて確認する方法を示しています。
業務処理が複数段階なら、イベント単位の完了フラグ一つでは足りません。たとえば「CRM更新は成功、顧客メールは失敗、社内通知は未実行」という状態を残し、再処理では失敗した副作用だけを進めます。各副作用にevent ID + action typeの冪等キーを付け、外部APIが冪等キーを受け付ける場合は同じ論理操作に同じ値を使います。
受信後に発生する例外を、機械的な再試行と人の判断へ分ける方法はAIエージェントの例外処理設計にも共通します。認証切れや一時的なtimeoutは回数制限付き再試行へ、契約状態の矛盾や金額の判断が必要なイベントは自動再処理せず担当者確認へ送ります。
失敗イベントを隔離して段階的にリプレイする
通常キューで同じイベントを無制限に回すと、恒久的な入力エラーが処理能力を占有し、正常イベントまで遅れます。試行回数、経過時間、エラー種別の上限を超えたイベントは、デッドレターキュー(DLQ)または同等の隔離領域へ移します。隔離は廃棄ではなく、原因、修正、再処理、完了確認まで追跡するための待避です。
AWS EventBridgeは、ターゲットへの配信を既定で最長24時間・最大185回再試行し、指数バックオフとジッターを使います。再試行を使い切ったイベントは、DLQを設定していなければ破棄されます。EventBridgeのDLQメッセージ属性には、エラーコード、エラーメッセージ、再試行回数、ルールやターゲットの識別情報などが含まれ、失敗理由の切り分けに使えます。SQSではredrive policyのmaxReceiveCountを小さくしすぎると、一時的な失敗でもDLQへ送られるため、処理時間と障害回復時間に合わせます。
| 失敗分類 | 例 | 自動再試行 | リプレイ前の条件 |
|---|---|---|---|
| 一時的な外部障害 | timeout、5xx、短時間のrate limit | 回数・時間を制限し、backoffとjitterを使う | 依存先の回復とキュー容量を確認 |
| 認証・権限エラー | 期限切れtoken、権限変更、接続先の無効化 | 同じ資格情報で繰り返さない | 認証を修正し、最小権限と接続テストを確認 |
| 入力・schemaエラー | 必須項目欠落、型変更、未知のevent type | 原則として即時隔離 | 互換処理または変換方針をレビュー |
| 業務矛盾 | 対象契約が存在しない、状態遷移が不正 | 自動で推測しない | 担当者が正しい処理または却下を決定 |
| 容量・流量超過 | 大量再送、worker不足、DB接続枯渇 | 入口を抑え、処理量を制御 | 安全な再処理速度と停止閾値を決める |
リプレイは、DLQ全件を一度に戻す操作にしません。対象期間、provider、event type、エラー原因、tenantで範囲を固定し、まず数件のcanary batchを再処理します。成功率、処理時間、重複判定件数、外部APIのrate limit、通常キューの遅延を確認してから速度を上げます。AWS SQSのDLQ redriveは、元キューまたは同じ種類の別キューへ戻し、移動速度を制御できます。AWSも最初は低い速度から始めることを勧めています。
- 障害時間と対象を固定する:開始・終了時刻、送信元、endpoint、event type、tenant、影響業務を記録します。
- 新規流入を保護する:通常イベントを止めるか、通常キューとリプレイ用キューを分離し、復旧作業が現在の処理を圧迫しないようにします。
- 根本原因を修正する:認証、schema、コード、外部依存、容量不足を直し、同じ失敗が再現しないことをテストします。
- 冪等性を先に試す:処理済みイベントを1件再投入し、業務レコードや通知が増えず、処理済みとして安全に完了することを確認します。
- 少量を再処理する:未処理イベントを小さなbatchで流し、処理結果IDまで追跡します。
- 段階的に速度を上げる:通常処理の遅延、エラー率、rate limit、DB負荷、重複防止の作動件数を見ながら調整します。
- 停止条件を守る:同一エラーの再発、重複副作用、通常キュー遅延、件数差の拡大が起きたら、残りを隔離したまま停止します。
再処理中にevent IDやenqueue時刻が変わる製品もあります。SQSのredriveでは、再投入されたメッセージに新しいmessage IDとenqueue timeが付くと案内されています。そのため、キュー固有IDだけを追跡キーにせず、元のprovider event ID、元DLQ message ID、今回のreplay run IDを別々に残します。
再処理後は件数と業務結果を照合する
再処理APIが200を返した、DLQが空になった、送信側でdeliveryが成功になった、という一つの事実だけでは完了ではありません。元の対象集合を固定し、入口から最終結果まで同じ単位で差分を求めます。
再処理は、失敗イベントをもう一度流した時ではなく、元の配信台帳と最終業務結果がevent ID単位で一致した時に完了します。
| 照合対象 | 合格条件 | 差がある場合の確認 |
|---|---|---|
| 送信側の対象イベント | 対象期間・event type・tenantの集合が確定 | 時刻境界、ページネーション、保持期限、timezone |
| 受信台帳 | 対象event IDが受信済みまたは意図した却下状態 | 未配信、保存前失敗、重複ID、別endpoint |
| 処理状態 | 成功、手動解決、対象外のいずれかで終端 | 処理中のまま、再試行待ち、DLQ残存 |
| 業務結果 | 必要なCRMレコード、請求、通知、ファイルが1回だけ存在 | 部分成功、二重作成、取消済み、後続処理待ち |
| 副作用別の件数 | expected、completed、intentionally skippedの和が一致 | 一つのイベントに複数副作用がある場合の欠落 |
| 残存失敗 | DLQ・保留・unknownがゼロ、または承認済み例外だけ | 所有者、期限、次の判断がない放置イベント |
件数は「送信100件、受信100件」だけでなく、event ID集合の差分で確認します。重複配送があれば配送回数は増えても論理イベント数は増えません。逆に、送信側100件と受信側100件でも、別々のevent IDが一件ずつ欠落・重複していれば件数だけでは見抜けません。expected set、received set、completed set、skipped setを保存し、集合差分をゼロにします。
フォーム、通知、CRMのように複数経路へ影響する場合は、入口から最終到達までを監視するフォーム送信の到達監視の考え方も役立ちます。顧客影響が続く障害では、復旧作業とは別に障害時のステータスページ運用を使い、確認済みの影響範囲、暫定回避、次回更新時刻を外向きに案内します。
よくある質問
Webhookの配信失敗と受信後の処理失敗をどう切り分けますか?
送信側のdelivery ID・event ID・応答コードと、受信側のイベント台帳を照合します。受信台帳にevent IDがなければ送信側から再送し、保存済みで処理状態だけ失敗なら内部キューまたはDLQから再処理します。最終業務結果がすでにある場合は副作用を再実行しません。
再送で同じ処理を二重実行しないために何を記録しますか?
provider、event ID、event type、object ID、payload hash、処理状態、各副作用の冪等キー、最終結果IDを記録します。providerとevent IDへ一意制約を置き、完了済みイベントは成功応答へ着地させても、請求、通知、CRM登録は再実行しない設計にします。
失敗イベントはどこへ隔離し、いつ再処理しますか?
回数または時間の上限を超えたイベントをDLQなど通常キューと分かれた領域へ移します。原因を修正し、通常処理の容量、冪等性、停止条件を確認した後、対象を絞って少量から再処理します。入力不正や業務判断が必要なイベントは、自動で戻さず担当者確認へ送ります。
再処理後に取りこぼしがないことをどう検証しますか?
対象期間のevent ID集合を固定し、送信側、受信台帳、処理状態、最終業務結果を照合します。expected、completed、intentionally skippedの和を一致させ、DLQ、再試行待ち、unknownに所有者不明のイベントが残っていないことを確認します。
送信サービスの手動再送を実行すれば自動再試行は止まりますか?
サービスごとに異なります。Stripeでは手動再送を実行しても自動再試行は止まりません。GitHubは失敗配送を自動再送しません。管理画面のボタンだけで判断せず、対象サービスの公式仕様、配送履歴、受信側の冪等性を確認してください。
DLQを空にすれば復旧完了ですか?
完了ではありません。DLQから移しただけで、業務処理が成功していない場合があります。元event ID、replay run ID、処理結果IDを結び、最終業務結果が一度だけ作られたこと、残存失敗がないことを確認します。
仕様を確認する公式資料
再試行期間、配送履歴の保持、再配信方法、DLQの機能はサービスごとに変わります。2026年8月13日時点で確認した次の公式資料を基に、自社で使うサービスの最新仕様を確認してください。
- Stripe Docs:Receive Stripe events in your webhook endpoint
- GitHub Docs:Handling failed webhook deliveries
- GitHub Docs:Redelivering webhooks
- GitHub Docs:Best practices for using webhooks
- AWS:Event retry policy and using dead-letter queues
- AWS:EventBridge dead-letter queues
- AWS:Configuring a dead-letter queue redrive
Webhook連携の復旧手順を整えたい場合
送信側の配送履歴、受信台帳、内部キュー、業務結果が別々に管理されていると、障害時に再送対象を安全に絞れません。通常時の保存項目、冪等性、DLQ、リプレイ速度、照合条件を一つのrunbookへまとめると、担当者が変わっても二重実行と取りこぼしを防ぎやすくなります。