エンジニアリング · 2026年9月21日
At-Least-Once配信を前提としたメールWebhookの設計
再試行、冪等キー、署名検証を組み合わせて、配信イベントを1件も取りこぼさない堅牢なメールイベント用Webhookコンシューマーを構築する方法を解説します。
イベントを確実に届けることの難しさ
メールWebhookでAt-Least-Once配信を実現するには、送信側が失敗したリクエストを指数バックオフで再試行し、受信側が冪等性を保証する仕組みを実装する必要があります。ネットワークは不安定で、サーバーはクラッシュするものです。HTTP 200 OKが1回返ったからといって、イベントが処理されたと保証されるわけではありません。信頼性は、送信側の永続的な再試行キューと、受信側の重複排除レイヤーを組み合わせることで実現します。
SendHQのようなメールAPIを組み込む場合、アプリケーションは、メールが配信されたのか、バウンスしたのか、迷惑メールとして報告されたのかを知る必要があります。これらのイベントは非同期で発生します。トラフィックの急増時にWebhookエンドポイントが5分間ダウンしただけで、重要な配信シグナルを何千件も失う可能性があります。その結果、分析データに欠落が生じ、システムがバウンスに対応できなくなります(バウンスへの対応は送信者レピュテーションの維持に不可欠です)。
信頼性の高いWebhookの構成要素
堅牢なWebhookアーキテクチャは、署名検証、冪等な処理、再試行戦略という3つの柱で構成されます。
1. 署名検証
IPアドレスや、本文にAPIキーが含まれているかどうかだけを根拠に、WebhookエンドポイントへのPOSTリクエストを信頼してはいけません。攻撃者はこれらを偽装できます。代わりに、HMAC(ハッシュベースのメッセージ認証コード)署名を使用してください。
送信側は共有の秘密情報を使ってペイロードに署名し、その署名をヘッダー(例:X-SendHQ-Signature)に付加します。受信側は同じ秘密情報を使ってハッシュを再計算し、ヘッダーの値と比較します。
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}
2. 冪等性と重複排除
At-Least-Once配信とは、送信側が成功レスポンスを受け取るまでイベントを送り続けることを意味します。サーバーがイベントを処理したものの、200 OKを返す前にクラッシュした場合、送信側は同じイベントを再送します。冪等性がなければ、1回の配信をデータベース上で2回とカウントしてしまうかもしれません。
すべてのイベントには一意のevent_idが必要です。処理済みのイベントを追跡するには、冪等キーのパターンを使用してください。
処理の流れ:
- Webhookのペイロードを受信する。
event_idがprocessed_eventsテーブルに存在するかを確認する。- 存在する場合は、直ちに200 OKを返し、本文は無視する。
- 存在しない場合は、単一のトランザクション内でイベントを処理し、
event_idを記録する。
3. 再試行戦略
送信側から見ると、再試行ポリシーは必須です。標準的なパターンは、ジッター付きの指数バックオフです。たとえば、1分後、5分後、30分後、2時間後、12時間後に再試行します。
受信側が4xxエラー(429を除く)を返した場合、通常はクライアント側のエラー(署名の不正など)を示しており、再試行しても解決しません。5xxエラーやタイムアウトは一時的な失敗を示しており、再試行が不可欠です。
ペイロードの具体例
SendHQから受け取る典型的な配信イベントのペイロードは次のとおりです。
{
"event_id": "evt_12345abcde",
"event_type": "delivered",
"timestamp": "2026-09-15T10:00:00Z",
"message_id": "msg_98765xyz",
"recipient": "user@example.com",
"metadata": {
"order_id": "ord_5544"
}
}
障害とエッジケースへの対処
「遅いコンシューマー」問題
Webhookハンドラーが重いデータベース書き込みを行ったり、他の外部APIを同期的に呼び出したりすると、エンドポイントがタイムアウトします。これが送信側の再試行ロジックを発動させ、サーバーをダウンさせかねない「再試行の嵐」を引き起こします。
解決策:受け付けと処理を分離します。
- Webhookを受信する。
- 署名を検証する。
- 生のペイロードをメッセージキュー(RabbitMQ、SQS、Redisなど)に投入する。
- 直ちに200 OKを返す。
- 別のワーカープロセスがキューを読み取り、データベースを更新する。
エージェント対応の問題
AIエージェントがWebhookをトリガーに動作する場合、無限ループのリスクが高まります。エージェントが「delivered」イベントを受け取って別のメールを送信し、それがさらに「delivered」イベントを発生させると、ループが生じます。
メール送信は外部への副作用として扱ってください。エージェントは、ヒューマン・イン・ザ・ループによる承認や、その操作が必要であることを確かめる厳格なステートマシンのチェックなしに、Webhookに基づいて自動でメールを送信すべきではありません。
各サービスの比較
プロバイダーを選ぶ際、信頼性は、これらのイベントをどう扱うか、そしてイベントを生み出すメールの送信量に対していくら請求するかと密接に関係しています。
大量のトランザクションメールでは、コストの差は歴然としています。Amazon SESの料金によると、à-la-carteでの送信は1,000通あたり0.10 USDです。一方、Postmarkの料金は月額15 USDで10,000通からで、超過分は1,000通あたり1.20〜1.80 USDです。50,000通の送信量では、SESのà-la-carteがおよそ5 USDであるのに対し、Postmarkのプランは約66 USDになります。
その他の選択肢として、Resendは月3,000通(1日100通まで)の無料プランと、月額20 USDで50,000通のProプランを提供しています。Mailgunは月額15 USDで10,000通からです。SendGridは無料プランを60日間のトライアルに変更しており、Essentialsは月額19.95 USDからです。
どのプロバイダーを使うにしても、データの整合性を左右するのは、これらのイベントを受け取る側の信頼性です。
エンジニア向け実装チェックリスト
- 署名の検証:共有秘密情報と定数時間比較関数を使用してペイロードを検証していますか?
- 非同期処理:エンドポイントは、重いビジネスロジックを実行する前に200 OKを返しますか?
- 冪等性:重複処理を防ぐために、
event_idに一意制約がありますか? - タイムアウト管理:再試行の重複を避けるため、タイムアウトはプロバイダーのタイムアウトより短く設定されていますか?
- 監視:Webhookエンドポイントの5xxレスポンス急増に対するアラートはありますか?
- DNSの健全性:受信サーバーは正しく設定されていますか?SendHQ Email DNS Checkerなどのツールを使用して、インフラに到達可能で正しく設定されていることを確認してください。
- 認証標準:送信メールが受け付けられるよう、DKIM、SPF、DMARCを実装し、処理する「バウンス」Webhookの数を減らしていますか?
トレードオフのまとめ
アプローチ | 長所 | 短所
同期処理 | 実装が簡単、即時の一貫性 | タイムアウトのリスクが高い、再試行の嵐が起きやすい
キューベースの処理 | 高いスケーラビリティ、急増に強い | インフラが複雑になる、結果整合性
単純なログ記録 | オーバーヘッドが小さい | 手動のログなしでは取りこぼしたイベントを復旧できない
冪等性テーブル | データの整合性を保証 | イベントごとにデータベースへの書き込みが1回増える
まとめ
メールWebhookの信頼性とは、障害を防ぐことではなく、障害を前提に設計することです。ネットワークは失敗するものであり、イベントは複数回届くものだと想定することで、真に回復力のあるシステムを構築できます。小さなプロジェクトのSPFレコードを管理する場合でも、大規模なトランザクションシステムをスケールさせる場合でも、署名検証と冪等性のパターンは変わらず定番です。
SendHQでメールインフラを構築しましょう。