独立站Webhookの信頼性:再試行、冪等性、署名検証、監視
一、Webhookが本質的に信頼できない理由
Webhookは相手のシステムが能動的にイベントをあなたのコールバックURLにPOSTする仕組みです。HTTPの上に成り立っていますが、HTTP自体はコネクションレスで到達を保証しません。ネットワークが一瞬乱れる、サービスが再起動する、処理がタイムアウトする、それだけでこの配信は失われる可能性があります。さらに厄介なのは「相手が受け取ったかどうか分からない」ことです。送信したのに2xx応答が返らないとき、ネットワークで失われたのか、相手が処理したが応答だけ失われたのか、判別できません。
ここでの直感に反する判断はこうです。Webhookシステムはデフォルトで再試行するため、受信側は「重複配信される」前提で設計する必要があります。一度しか来ないと仮定してはいけません。StripeやShopifyのような成熟したプラットフォームの標準的な方法は、受信側が2xxを返さなければ指数バックオフで再送し、最大5〜8回、約24時間にわたって試行するというものです。
冪等性を実装していなければ、同じ決済イベントが何度も計上されます。リードのシステム連携についてはサイトのリードから CRM へをご覧ください。そこでも同じく重複イベントに対処する必要があります。
四つの要素のどれか一つが欠けると、信頼性は一段下がります。次の表で各要素の「やること」と「漏れたときの結果」を整理します。
| 要素 | 役割 | 標準的な実装 | 漏れたときの結果 |
|---|---|---|---|
| 署名検証 | リクエストが本当に相手から来たか確認する | HMAC-SHA256でヘッダーを検証 | 偽造イベントで不正な注文が入る |
| 即時応答 | 相手のタイムアウト再送を防ぐ | 先に2xxを返し、その後で非同期処理 | 処理が遅くて重複配信が起きる |
| 冪等な重複排除 | 重複イベントを一度だけ処理する | イベントIDをテーブルに記録して排除 | 同じ注文が何度も計上される |
| 再試行バックオフ | 一時的な失敗でも最終的に届く | 指数バックオフにジッターを加える | 偶発的な障害で注文がそのまま失われる |
| デッドレター監視 | 失敗イベントを黙って捨てない | 上限超過はデッドレターキューに入れてアラート | 長期的に注文が失われても誰も気づかない |
二、署名検証:このリクエストが他人の偽造でないことを確認する
Webhook のコールバック URL は誰でもアクセスできる公開 URL で、誰でもそこに POST できます。署名検証をしないと、攻撃者が「決済成功」の偽イベントを送りつけるだけで、管理画面側が本当に権限を付与したり注文を通してしまう恐れがあります。これは理論上のリスクではなく、実際に攻撃されたことのある攻撃面です。
標準的な方法は HMAC-SHA256 です。相手が取り決めた秘密鍵でリクエストボディに署名し、リクエストヘッダー(例: X-Signature)に付けます。受け取った側は同じ鍵で元のリクエストボディを再計算し、両方を照合して一致したときだけ処理します。
押さえるべき点は2つあります。1つ目は、通常の文字列比較(==)ではなく定数時間比較関数を使い、タイミング攻撃を防ぐこと。2つ目は、リクエストヘッダーのタイムスタンプを検証し、5分を超えるずれは即座に拒否して、古いリクエストを録音して再利用するリプレイ攻撃を防ぐことです。鍵が漏れたらすぐにローテーションできるようにしておきます。
| 検証項目 | やり方 | 不合格時の対応 |
|---|---|---|
| 署名が一致するか | HMAC-SHA256 で再計算して照合 | そのまま 401 で拒否 |
| タイムスタンプが新しいか | サーバー時刻との差が5分以内 | 拒否してリプレイを防ぐ |
| 鍵が期限切れか | 新旧の鍵を並行してローテーション | 古い鍵で再試行 |
| リクエストボディが改ざんされていないか | 元のバイト列で署名を計算 | 先に JSON デコードしてから署名しない |
三、即時応答と非同期処理: リクエスト内で重い処理をしない
ここが最もつまずきやすいポイントです。Webhook を受け取ったあと、同期リクエストの中で DB 検索、メール送信、外部 API 呼び出し、ログ書き込みと重い処理を一通りこなして5秒かかってしまう人が多いです。相手は3秒待っても 2xx が返らないとタイムアウトと判断して再送を始めます。こちらはまだ1件目を処理中なのに、2件目がまた届くという状態です。
正しいのは3段階のパターンです。リクエストを受け取ったらまず署名を検証し(ミリ秒単位)、検証が通ったらすぐにイベントをそのまま DB に保存して 2xx を返し、そのあとバックグラウンドの非同期タスクでゆっくり処理します。こうすれば相手は3秒以内に必ず確認を受け取れ、再送は起きません。重い処理が遅くても、ユーザーにはすでに応答が返っているので問題ありません。処理に失敗したイベントはキューに残し、非同期タスクが自分でリトライします。
シナリオ例:決済成功イベントが届きます。受信側がやることは3つだけです。署名を検証し、イベント ID と元の payload を events テーブルに書き込み、すぐに 200 を返します。実際の「在庫追加、メール送信、ERP 通知」はバックグラウンドの worker が events テーブルから未処理イベントを取り出してゆっくり行います。メール送信に10秒かかっても、相手の再送には影響しません。フォーム送信の信頼性の考え方については、次と照らし合わせてください。問い合わせフォームの信頼性。
| フェーズ | アクション | 所要時間の目標 | 失敗時の対応 |
|---|---|---|---|
| 同期受信 | 署名検証 + DB 保存 + 2xx 応答 | 300 ミリ秒未満 | 署名検証に失敗したら即拒否 |
| 非同期処理 | DB 検索、通知送信、下流呼び出し | 秒単位から分単位まで可 | 失敗したらキューに残してリトライ |
| リトライ | バックオフ間隔で再実行 | 指数バックオフ | 上限を超えたらデッドレターキューへ |
| 手動での再処理 | デッドレターキューを確認して手動実行 | 毎日の点検 | 再処理の理由を記録 |
四、冪等性とリトライ: 同じイベントが10回来ても1回しか処理しない
冪等とは、同じ操作を1回実行しても10回実行しても結果がまったく同じになることです。Webhook は本質的に冪等でなければなりません。相手の再送を止められないからです。実装は簡単で、相手が各イベントにグローバルに一意なイベント ID を付けるので、処理前にこの events テーブルを確認し、その ID を見たことがあれば成功を返して処理をスキップします。
再試行戦略は指数バックオフとジッターを組み合わせます。1回目の失敗は1分待ち、2回目は2分、3回目は4分と倍にしていき、最大5〜8回、合計で約24時間です。ランダムなジッターを加えるのは、失敗したイベントが同じ秒に集中して再試行され、サービスをもう一度圧迫するのを防ぐためです(これを再試行ストームと呼びます)。再試行の上限を超えたイベントは捨てずにデッドレターキューに入れ、人の対応を待ちます。
| 再試行回数 | バックオフ間隔(約) | 累計時間 | 説明 |
|---|---|---|---|
| 1回目 | 1分 | 1分 | 一時的なネットワークの揺らぎ |
| 2回目 | 2分 | 3分 | 短時間のサービス再起動 |
| 3回目 | 4分 | 7 分 | 下流が一時的に利用不可 |
| 4回目 | 8分 | 15分 | 比較的長い障害 |
| 5〜8回目 | 15〜30分 | 約24時間 | それでも失敗ならデッドレターキューへ |
五、監視とデッドレター:注文の取りこぼしを黙って起こさない
Webhook で最も厄介な障害は「静かな取りこぼし」です。相手は送信に成功したと思っているのに、こちらには記録が1件もなく、数週間後に顧客から「支払ったのになぜ発送されないのか」と苦情が来て、イベントがそもそも届いていなかったと気づきます。だから監視では3つを必ず見ます。配信成功率、再試行キューの滞留、デッドレターキューの件数です。
しきい値は具体的な数字で設定します。再試行キューの滞留が100件を超えたら、または30分間連続で成功した配信が1件もなければ、普段使うチャネルにすぐアラートを送ります。デッドレターキューは毎日1回点検し、滞留があれば手動で再処理します。配信成功率が99%を下回ったら調査が必要です。健全な状態の Webhook 成功率は99.9%に近いはずで、99%を下回るなら継続的な問題があります。汎用的な監視アラートの組み方は次を参照してください。サイト監視とアラート。
| 監視指標 | 健全性 | アラートしきい値 | アクション |
|---|---|---|---|
| 配信成功率 | 99.9%以上 | 1時間連続で99%未満 | 受信側のログを確認 |
| 再試行キューの滞留 | ほぼ0 | 100件超 | 下流が詰まっていないか確認 |
| デッドレターキュー | 0 | 滞留が1件でも発生 | 手動で再処理し原因を特定 |
| 成功配信が連続でなし | イベントは継続的にあり | 30分間で0件 | コールバック URL がブロックされていないか確認 |
| 署名検証の失敗回数 | ほぼ0 | 10倍に急増 | 誰かが API をスキャンしている可能性 |
FAQ よくある質問
Webhook で時々重複した注文イベントを受け取るのはなぜですか?
Webhook を受け取ったらすぐ処理すべきですか、それとも先に返すべきですか?
HTTPS経由なら署名検証は不要では?
リトライ間隔はどう決める?なぜ毎分固定のリトライではだめなのか?
何度リトライしても失敗するイベントはどう扱う?
Webhookが黙って配信を落としていないか、どうやって気づく?
最後に
Webhookの信頼性の核心は、「一方通行のプッシュではなく、確認がなければ再送する双方向プロトコル」だという認識にある。受信側を設計するときは、「重複配信される」「偽造される」「失われる」を例外ではなくデフォルトの前提としなければならない。4つの要素のうち、冪等性による重複排除と迅速な確認応答は最も見落とされやすく、トラブルが最も多い。まずこの2つを正しく実装すれば、事故率は大きく下がる。
導入は3ステップ。第一に、受け側は署名検証・DB保存・3秒以内に2xxを返すことだけを行い、業務処理はすべて非同期にする。第二に、イベントIDで重複排除テーブルを作り、リトライは指数バックオフとジッターを組み合わせる。第三に、デッドレターキューを毎日点検し、配信成功率と滞留量をアラートに接続する。この3ステップを終えれば、コールバック経路は「たまに取りこぼして運任せ」から「取りこぼしても当日中に気づき、取り戻せる」状態になる。
覚えておきたい一言。Webhookの信頼性は、相手が安定して送ってくれることではなく、こちらが愚直に受け止めることにかかっている。署名を検証し、素早く返し、重複を排除する。二重処理になってもいいから、一件も取りこぼすな。
