Webhook2026年4月18日8 分で読めます

Stripe Webhook のリトライ:本番運用ガイド

Stripe は失敗した Webhook を最長 3 日間リトライします。スケジュールの仕組み、エンドポイントがダウンしたときに壊れるもの、それに備えた設計を解説します。

試行の間隔 · 指数バックオフ現在5 分30 分3 時間18 時間打ち切りstatus=failed · ペイロードはログに残る · 再送もできる
3 日間のリトライの後は、沈黙。その沈黙が問題になるかどうかは、自分のログが決める。

サーバーがダウンしているときに Stripe が payment_intent.succeeded を送ったら、そのイベントはどうなりますか?Stripe は永遠にリトライしますか?期間はどのくらいですか?取りこぼしたイベントは、どう照合すればよいですか?

これは、Stripe を組み込む開発者から最もよく聞かれる質問です。処理し損ねた Webhook は、プロダクトが対応できない売上なので重要です。顧客は支払いを済ませたばかりです。それなのに注文を処理するジョブは動かず、顧客のダッシュボードも更新されません。

TL;DR

  • Stripe は指数バックオフで 3 日間リトライし、その後は二度とリトライしません
  • 200 をすばやく返し、処理は非同期で行い、処理の前にすべてのイベントを記録し、冪等にします
  • これをすべて作る時間がないなら、Webhook 中継サービスを使えば、URL を 1 つ変えるだけでリトライ、ログ、再送が手に入ります

Stripe のリトライの仕組み

Stripe は、Webhook エンドポイントがすばやく 2xx のステータスコードを返すことを期待しています。正確な数字は公開されておらず、ドキュメントには「タイムアウトの原因になりうる複雑なロジックの前に」2xx を返すように、としか書かれていません。コミュニティが目安にしている数字は 20 秒です。タイムアウトした配信は、5xx のレスポンスやネットワークの障害と同じく、失敗として数えられます。

失敗すると、Stripe は指数バックオフで最長 3 日間、イベントをリトライします。リトライの間隔はだんだん広がり、最後の試行は元のイベントからおよそ 72 時間後です。

3 日が過ぎると、Stripe はイベントを失敗として記録し、リトライをやめます。誰かを起こしてくれることも、代わりのメールを送ってくれることもありません。イベントは配信キューから消えるだけです。ただし、Stripe ダッシュボードの Developers → Events(開発者 → イベント)では引き続き確認できます。

Stripe のリトライ曲線は、穏やかなほうでもあります。Merchant of Record 型のプラットフォームは、もっと激しくリトライすることがあります:Paddle は約 2 日間で最大 60 回試行します。Stripe のスケジュールには追いつけるハンドラーでも、Paddle のスケジュールには押し流されることがあります。

失敗がそれ以上続くと、Stripe は最終的にエンドポイントそのものを無効にし、停止日をメールで知らせてきます。そこからの復旧は別の作業です。イベントは Events API から 30 日間取得できます:補完の手順は Stripe に Webhook エンドポイントを無効化されたときの記事を参照してください。

痛い目を見る 3 つの障害パターン

まず、エンドポイントが遅い場合です。Stripe は、公開されていないタイムアウトを過ぎたレスポンスを失敗として数えます。Webhook ハンドラーが、遅いデータベースへの書き込み、Salesforce への同期、顧客へのメール送信を同期的に行っているなら、イベントのたびにタイムアウトすれすれです。

次に、デプロイの時間帯です。デプロイ中にサーバーが再起動し、10 件ほどのイベントを取りこぼしてから復帰します。Stripe はそれらのイベントをリトライしますが、3 日間だけです。

そして、バグで 500 が返り続ける場合です。特定のイベントタイプで例外を投げるデプロイを出してしまいます。Stripe はリトライしますが、同じエラーに当たり続けます。3 日後に Stripe はあきらめ、誰にも気づかれないデータの欠落が残ります。

3 つのどのケースでも、Stripe は正しく動いています。失敗しているのは、パイプのこちら側です。

本番品質の連携がやっている 4 つのこと

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 は守れない約束検証200 を返すwaitUntil(work)メモリ上のみ回収 → 消失。二度とリトライされない先に永続化 · その 200 は本当検証永続化200 を返す処理リトライ可能
永続的に書き込んだものに対して 200 を返せば、プロバイダーの時計はビジネスロジックの制約ではなくなる。

2. 処理の前にすべてのイベントを記録する

ビジネスロジックに手を付ける前に、生のペイロードとヘッダーを永続化します。ロジックが例外を投げても、ログから再送できます。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;

INSERT の影響行数が 0 なら、このイベントは以前に見ています。スキップします。

リトライが同時に、あるいは順不同で届き始めると、冪等性は見た目より奥が深くなります。トランザクションの境界、ロールバックできない副作用のための claim-then-commit パターン、そして順不同の配信を直すのが重複排除ではなくバージョンガードである理由は、同じ Webhook を 2 回受け取ったときの記事で扱っています。

4. 失敗をアラートで知らせる

5 件連続でイベントが失敗したら、顧客がサポートにメールしてきたときではなく、数分以内に知りたいはずです。連続した失敗を追跡し、自分が呼び出される仕組みを作ってください。

AnyHook の位置づけ

AnyHook はサーバーの前に置かれ、これをトランスポート層で解決します。Stripe の送信先をオリジンではなく in.anyhook.net/you/stripe に向けると、AnyHook は次のことを行います:

  • Stripe に約 1 秒で 200 を返します。そのため応答期限が問題になることはありません
  • すべてのイベントを、ヘッダーとボディを含めて完全に記録し、保存時に暗号化します
  • サーバーへの配信を 5 分 → 30 分 → 3 時間 → 18 時間 → 以降は 1 日ごとにリトライします:試行は Free で 3 回、Pro で 5 回、Scale で 10 回です。Stripe 自身のスケジュールは最長 3 日間続くので、Free と Pro で長期の安全網になるのは、リトライ回数ではなく、保存されたログと再送です
  • クリック 1 回で再送できます。サーバーが 1 時間ダウンしていたなら、App(Webhook の送信元ごとに作る受信設定)のページでボタンを 1 回押せば、その 1 時間に失敗したイベントを再送できます(一度に最大 100 件)。失敗したイベントの再送は、月間クォータにカウントされません。ただし 1 日の再送上限があり、コンピュート枠を消費します
  • 連続した失敗を通知します。1 回と 5 回連続で失敗した時点でメールを送ります。20 回になると、その転送先への配信を一時停止し(最初は 10 分、繰り返し作動するとより長く)、再びメールを送ります。一時停止中に届いたイベントは配信されず、再送できるように記録されます

SDK は不要で、URL を 1 つ差し替えるだけです。最初の配信は、既存の Stripe の署名チェックをそのまま通ります。リトライと再送には Stripe の元のタイムスタンプが付いていて、Stripe の SDK は 5 分を過ぎるとそれを拒否します。それらを受け入れるには、AnyHook-Signature ヘッダーを検証してください。

何かに登録する前に試すなら、アカウントなしで、コマンド 1 つで実際に使える受信 URL が返ってきます:

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

レスポンスには、プロバイダーに設定する inbound_url、API 経由でイベントログを読むための api_key、そして claim_url が含まれます。このエンドポイントは一時的なものです:紐付け用の URL を開いてログインし、アカウントに紐付けない限り、エンドポイントとそのイベントは 7 日後に削除されます。紐付ければ同じ URL をそのまま使えます。

まとめ

いま Stripe のイベントを失っているなら、たいていの対処はリトライを増やすことではありません。処理の前に永続化して、再送の元になるものを常に手元に置くことです。

すべての記事2026年4月18日 · 8 分

Webhook を、もう取りこぼさない。

URL を 1 つ変えるだけで、リトライ、イベントログ、1 クリックでの再送が使えます。先に動きを見ることもできます。新規登録して 30 秒の障害訓練を実行すると、サーバーがダウンしていてもイベントが失われない様子を確認できます。サーバーを用意する必要はありません。