再送・順序・冪等性の設計

Webhook 再送 冪等性 順序 リトライ
この記事の対象
Webhook受信側を実装する開発者向けです。「同じイベントが複数回届いた」「古いイベントが新しいイベントの後に届いた」といった状況で正しく動作する受信側の作り方を説明します。

Webhookは便利な仕組みですが、ネットワークは不安定で、サーバーは落ちるものです。レシートローラーは「届かなかった」ときに自動で再送する仕組みを持っていますが、その結果として同じイベントが2回以上届くこと順序が前後することが起こり得ます。受信側はこれを前提に設計してください。

再送ポリシー

受信側が 2xx を返さなかった場合、レシートローラーは指数バックオフで再送を試みます。

送信タイミング
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 を返すと再送されません。

順序は保証されない

レシートローラーWebhookは、配信順序を保証しません。例えば次のような状況が起こり得ます。

  • receipt.issued より先に receipt.refunded が届く
  • product.updatedproduct.created より先に届く
  • 1回目の配信が再送され、新しいイベントの後に旧イベントが届く

順序を扱う必要がある業務では、ペイロードに含まれる occurred_at(イベント発生時刻)を見て受信側で並べ替えるか、APIで最新状態を取り直す設計にしてください。Webhookを「最新状態の通知」ではなく「変更があったというトリガー」として扱うのが安全です。

冪等性の実装

同じイベントが複数回届いても結果が変わらないようにすることを冪等性と呼びます。レシートローラーは各イベントに一意の 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. キューワーカーで業務処理を実行(リトライは内部で)

このパターンなら、受信エンドポイントは常に高速に応答でき、業務処理の失敗が再送ループに繋がりません。

関連ガイド

公開日: 2026-04-27 更新日: 2026-04-27
タグ
API (8) Webhook (8) api (6) oauth (5) トラブル (5) OAuth (4) getting-started (4) アプリ登録 (4) app-registration (3) webhook (3)
関連記事