ブログ · · 約 11 分で
Webhook の再送の設計:間隔・ゆらぎ・冪等性・送信先の自動停止
Webhook の
要点
- 再送で
決める ことは、 再送の 間隔、 諦めるまでの 期間、 重複を 見分ける ID、 失敗が 続く 送信先の 止め方の 4 つです。 - 再送の
期間は 各社で 約 9 分から 25 日まで 開きが あり、 Stripe と SmartHR は 約 3 日、 Svix は 約 28 時間です。 - 間隔は
指数バックオフに ゆらぎを 加えるか 送信先ごとの 同時送信数を 絞り、 再送でも 同じ webhook-id を 送ります。 失敗が 続く 送信先は 一時停止と 自動の 無効化の 2 段階で 止めます。
Webhook の
各社の再送の仕様
まず、
| サービス | 再送の |
タイムアウト | 受ける |
|---|---|---|---|
| Stripe | 本番は |
記載なし | Stripe-Signature |
| SmartHR | 本番は |
60 秒 | X-SmartHR-Token |
| KOMOJU | 最大 25 回。 |
記載なし | X-Komoju-Signature |
| PAY.JP | 3 分間隔で |
記載なし | X-Payjp-Webhook-Token |
| Shopify | 4 時間で |
5 秒 | X-Shopify-Hmac-SHA256 |
| GitHub | 自動の |
10 秒 | X-Hub-Signature-256 |
出典: Stripe、
再送の
国内で
再送の間隔と期間
指数バックオフ
間隔は、
計算は基準 × 2^(回数) に
const BASE_MS = 5_000; // 最初の間隔
const CAP_MS = 10 * 3600_000; // 1 回の間隔の上限(10 時間)
const MAX_ATTEMPTS = 8; // 最初の送信を含めた回数
/** attempts 回失敗した後の待ち時間。諦めるなら null */
export function backoffMs(attempts: number): number | null {
if (attempts >= MAX_ATTEMPTS) return null;
const exp = Math.min(CAP_MS, BASE_MS * 2 ** (attempts - 1));
return Math.floor(Math.random() * exp); // full jitter
}ゆらぎ(jitter)
最後のMath.random() がrandom(0, min(cap, base * 2 ** attempt)) を、
ゆらぎを
再送を使い切った後
再送を
タイムアウトと成功の判定
- タイムアウト: 15 秒前後が
多い (Svix は 15 秒、 GitHub は 10 秒、 Shopify は 5 秒) 。 長く すると 送信側の ワーカーが 遅い 送信先に 占有されます。 - 成功の
判定 : 2xx だけを成功にします。 3xx は リダイレクトを 追わずに 失敗と して 扱います。 追うと、 登録時に 検査した URL と 違う 宛先へ 送る ことになる ためです。 - 応答の
本文 : 記録するのは先頭の 1KB 程度で 十分です。 受ける 側の エラーの 手が かりには なり、 記録の 容量は 膨らみません。
export async function sendOnce(url: string, headers: Record<string, string>, body: string) {
const started = Date.now();
try {
const res = await fetch(url, {
method: 'POST',
headers,
body,
redirect: 'manual',
signal: AbortSignal.timeout(15_000),
});
const text = (await res.text()).slice(0, 1024);
return { ok: res.status >= 200 && res.status < 300, status: res.status, head: text, ms: Date.now() - started };
} catch (e) {
return { ok: false, status: null, head: null, ms: Date.now() - started, error: String(e) };
}
}冪等性と webhook-id
再送が
そのため送信側は、webhook-id ヘッダーにwebhook-timestamp)
受ける
順番も
失敗が続く送信先の止め方
解約した
- 一時的に
止める : 同じ(サーキットブレーカー) 送信先への 失敗が 続いたら 数十秒送らず、 その 後 1 通だけ試して 様子を 見る。 遅い 送信先が ほかの 顧客の 送信を 待たせないように する ためです。 - 無効に
する : 失敗が数日続いたら 送信先を 無効にし、 送信側の 担当者に 知らせる。 Svix は 5 日で 無効にします (Svix の ドキュメント )。
無効に
Webhook Admin での扱い
Webhook Admin は、
| 回 | 前の |
最初の |
|---|---|---|
| 1 | すぐ | 0 |
| 2 | 5 秒 | 5 秒 |
| 3 | 5 分 | 約 5 分 |
| 4 | 30 分 | 約 35 分 |
| 5 | 2 時間 | 約 2 時間 35 分 |
| 6 | 5 時間 | 約 7 時間 35 分 |
| 7 | 10 時間 | 約 17 時間 35 分 |
| 8 | 10 時間 | 約 27 時間 35 分 |
- 既定の
間隔は Svix と 同じ表で、 ゆらぎは 入れていません。 代わりに、 送信先ごとに 同時に 送る 数を 既定 10 本までにし、 同じ 送信先に 5 回続けて 失敗したら 30 秒送らずに 1 通だけ試します。 枠が 空くまでの 送信は キューで 待たせます。 - タイムアウトは
15 秒で、 2xx だけを 成功にします。 リダイレクトは 追いません。 410 も 失敗と して 再送します。 応答の 本文は 先頭 1,024 バイトを 記録します。 - 署名は
Standard Webhooks の webhook-id/webhook-timestamp/webhook-signature(HMAC-SHA256 の v1)です。 webhook-idは再送でも 同じ値で、 時刻と 署名は 送る たびに 作り直します。 鍵を 切り 替えた あと 24 時間は、 新旧の 鍵の 署名を 両方付けます。 - 失敗が
5 日続いた 送信先は 自動で 無効にします。 失敗が 1 時間続いた とき、 再送を 使い 切った とき、 自動で 無効に した ときに、 Slack・Teams・Chatwork・ メール・Webhook で 知らせます (同じ 送信先は 1 時間に 1 回まで) 。 - 再送を
使い 切った 送信は、 管理画面か API から 手で 再送できます。
API からIdempotency-Key を
curl -X POST https://api.webhookadmin.com/v1/messages \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-2026-0927-001" \
-d '{"consumer":"customer_123","event_type":"invoice.paid","payload":{"invoice_id":"inv_001","amount":12000}}'決めることの一覧
- 再送の
間隔 (指数バックオフ+ゆらぎ、 または 送信先ごとの 同時数の 制限) と、 諦めるまでの 期間 - 成功の
判定 (2xx のみ、 リダイレクトは 追わない) と タイムアウトの 秒数 - 再送でも
変わらない イベントの ID と、 受ける 側への 「重複は ID で 捨てる」 「2xx を 先に 返す」 の 案内 - 再送を
使い 切った イベントの 記録と、 手での 再送 - 失敗が
続く 送信先の 一時停止・無効化と、 その 知らせ方
再送・記録・
よくある質問
再送の期間はどれくらいにすればよいですか。
受ける
指数バックオフにゆらぎ(jitter)は必要ですか。
多くの
再送のたびに webhook-id を変えてもよいですか。
変えません。