エンジニアリング · 2026年9月21日
メールAPIの冪等キー
冪等キーを実装して、ネットワークの再試行時に重複メールが送られるのを防ぎます。ユーザーに大量のメールを送りつけることなく、分散システムの障害に対処する方法を解説します。
重複メールの問題
重複メールは、クライアントがリクエストを送信し、サーバーがそれを処理したものの、クライアントが成功レスポンスを受け取る前にネットワークが失敗したときに発生します。タイムアウトや5xxエラーを受け取ったクライアントは、リクエストを再試行します。冪等性がなければ、サーバーは再試行を新しいリクエストとして扱い、メールを再度送信します。冪等キーを使うと、サーバーは繰り返されたリクエストを認識し、副作用を再実行せずに元の結果を返せるため、これを防げます。
インシデント対応を担うエンジニアにとって、「重複メールの嵐」ほど厄介なものはありません。これは通常、上流プロバイダーの部分的な障害や、応答時間を遅らせるデータベースのデッドロックの際に起こります。信頼性のために設計した再試行ロジックが凶器と化し、ユーザーに大量のメールを送りつけ、送信者レピュテーションを損ないます。
冪等性がないと再試行が失敗する理由
分散システムでは、あらゆるAPI呼び出しに3つの障害点があります。
- リクエストがサーバーに届かない。
- サーバーはリクエストを処理したが、レスポンスが失われる。
- 処理の途中でサーバーがクラッシュする。
ケース1で再試行するなら安全です。ケース2で再試行すると重複送信になります。ケース3で再試行すると、クラッシュが起きた箇所によっては重複送信になる可能性があります。
メール送信は外部への副作用です。データベースでユーザー名を更新する処理(SET name = 'Alice'を使えば本質的に冪等です)とは異なり、メール送信は追加的な操作です。sendエンドポイントを呼び出すたびに、新しいメッセージが世界に1通生まれます。これを冪等にするには、送信の意図に対する一意の識別子、つまり冪等キーを導入する必要があります。
冪等キーの実装
冪等キーは、クライアントが生成してリクエストヘッダーで送信する一意の値(通常はUUID v4)です。サーバーはこのキーを使ってリクエストの状態を追跡します。
サーバー側の処理の流れ
- リクエストの受信:サーバーは
Idempotency-Keyヘッダーが存在するかを確認します。 - 照会:サーバーは高速にアクセスできるストア(Redisなど)でそのキーを照会します。
- キャッシュヒット:キーが存在する場合、サーバーはメール配信エンジンを呼び出さずに、キャッシュ済みのレスポンスを直ちに返します。
- キャッシュミス:サーバーはキーをロックし、メール送信を処理し、レスポンスを保存してクライアントに返します。
- 有効期限:データベースが際限なく肥大化しないよう、キーは一定期間(例:24時間)後に失効するよう設定します。
ペイロードの具体例
SendHQのようなAPIを使う場合、リクエストは次のようになります。
POST /v1/send
Host: api.sendhq.cc
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer YOUR_API_KEY
{
"to": "user@example.com",
"template_id": "welcome-email",
"variables": {
"name": "Alex"
}
}
エラーケースへの対処
すべての再試行を同じように扱うべきではありません。クライアントエラーとサーバーエラーを区別する必要があります。
- 4xxエラー:サーバーが400(Bad Request)または422(Unprocessable Entity)を返した場合、リクエストは無効です。同じキーで再試行すると、同じ4xxエラーが返るはずです。ペイロードを変更して同じキーを再利用しないでください。競合が発生します。
- 5xxエラー:サーバーが500または503を返した場合、クライアントは再試行すべきです。サーバーがすでにメールをMTA(メール転送エージェント)に正常に引き渡していた場合でも、冪等キーによって再試行には200 OKが返り、2通目のメールは送信されません。
- 同時リクエスト:同じキーを持つ同一のリクエストがまったく同じミリ秒に2件届いた場合、サーバーは2件目に409 Conflictを返し、1件目がまだ処理中であることを示すべきです。
AIエージェントにおける冪等性
AIエージェント(MCPサーバーやA2Aカードを使うもの)は、新たなリスクの層をもたらします。LLMは非決定的であることがあり、ループ内で失敗したと認識すると、同じツール呼び出しを複数回実行することがあります。
エージェント対応の連携を構築する際は、承認ステップや、オーケストレーターが生成する決定的な冪等キーなしに、エージェントがsendアクションを実行できるようにしてはいけません。オーケストレーターは、エージェントの意図(例:「週次レポートをBobに送る」)を、レポートIDと日付に基づく安定したキーに対応付けるべきです。これにより、最初の呼び出しが失敗したとエージェントが「思い込んで」、同じレポートを誤って5回送ってしまう事態を防げます。
失敗のコスト:プロバイダーの比較
冪等性を実装しないと、ユーザーを困らせるだけでなく、お金も無駄になります。安価なプロバイダーもありますが、重複のコストはすぐに膨らみます。
各社の公式料金ページによると(2026年9月時点):
- Amazon SES:à-la-carteで1,000通あたり0.10 USDです(Amazon SESの料金)。2026年7月21日に導入された新しい料金プランには、Essentials(1,000通あたり0.16 USD)、Pro(1,000通あたり0.22 USDに加えてリージョンごとに月額105 USD)、Enterprise(1,000通あたり0.23 USDに加えて月額500 USD)があります。
- Resend:無料プランは月3,000通(1日100通まで)です。Proは月額20 USDで50,000通、超過分は1,000通あたり0.90 USDです(Resendの料金)。
- SendGrid:無料プランは現在60日間のトライアルで、Essentialsプランは月額19.95 USDからです(SendGridの料金)。
- Mailgun:月額15 USDで10,000通、超過分は1,000通あたり1.80〜1.10 USDです(Mailgunの料金)。
- Postmark:月額15 USDで10,000通、超過分は1,000通あたり1.80〜1.20 USDです(Postmarkの料金)。
具体的に言えば、50,000通の送信はSESのà-la-carteで約5 USD、Postmarkのプランでは約66 USDです。冪等性のない再試行ループによって誤って送信量が10倍になれば、プロバイダー間の費用差はインシデントレポートの中で無視できない項目になります。
到達率と受け付けの違い
冪等性が解決するのは受け付けの問題だけであることを理解しておくことが重要です。
- 受け付け:APIがリクエストを受け付け、200 OKを返します。冪等キーが機能するのはこの段階です。
- 配信:APIがメールを受信側サーバー(例:Gmail)に引き渡します。ここでSPFやDKIM/DMARCが重要になります。
- 受信トレイへの到達:受信側サーバーが、メールを受信トレイに入れるか迷惑メールフォルダに入れるかを判断します。
冪等キーが保証するのは、リクエストを1回だけ受け付けることです。メールが配信されることや、迷惑メールフォルダを避けられることを保証するものではありません。インフラが配信に向けて正しく設定されていることを確認するには、SendHQのメールDNSチェッカーなどのツールでレコードを検証してください。
エンジニア向け実装チェックリスト
メール送信ロジックを今まさに監査しているなら、次のチェックリストを使ってください。
- クライアント側でのキー生成:メール送信の一意な意図ごとにUUID v4を生成していますか?
- ヘッダーの実装:キーはリクエスト本文ではなく、標準ヘッダー(例:
Idempotency-Key)で渡されていますか? - ストレージ層:ストレージ肥大化を防ぐため、冪等キーにTTL(Time To Live)を設定していますか?
- アトミックロック:同じキーでの競合状態を防ぐため、サーバーは分散ロック(Redisの
SET NXなど)を使用していますか? - レスポンスのキャッシュ:再試行時にクライアントへ返せるよう、完全なレスポンス(ステータスコードと本文)を保存していますか?
- エージェントのガードレール:AIエージェントを使用する場合、キーはLLMではなくシステムオーケストレーターによって生成されていますか?
コード例:Node.jsの冪等性ミドルウェア
Node.js環境でRedisを使ってこのロジックを実装する方法の簡略化した例を示します。
const redis = require('redis');
const client = redis.createClient();
async function sendEmailHandler(req, res) {
const idempotencyKey = req.headers['idempotency-key'];
if (!idempotencyKey) {
return res.status(400).json({ error: 'Idempotency-Key header is required' });
}
// Try to acquire a lock and check for existing response
const cachedResponse = await client.get(`idempotency:${idempotencyKey}`);
if (cachedResponse) {
const { status, body } = JSON.parse(cachedResponse);
return res.status(status).json(body);
}
// Set a lock to prevent concurrent requests
const lock = await client.set(`lock:${idempotencyKey}`, 'true', 'NX', 'EX', 30);
if (!lock) {
return res.status(409).json({ error: 'Request is currently being processed' });
}
try {
// Actual email sending logic
const result = await emailProvider.send(req.body);
const responsePayload = {
status: 200,
body: result
};
// Cache the result for 24 hours
await client.set(`idempotency:${idempotencyKey}`, JSON.stringify(responsePayload), 'EX', 86400);
return res.status(200).json(result);
} catch (error) {
return res.status(500).json({ error: 'Internal Server Error' });
} finally {
await client.del(`lock:${idempotencyKey}`);
}
}
まとめ
トランザクションメールにとって、冪等性は「あれば便利」なものではありません。ユーザー体験とコスト管理を重視するあらゆるシステムにとって必須の要件です。一意性を担保する責任をクライアントに移し、サーバー側でその一意性を追跡する仕組みを用意することで、ネットワークが不安定なときの重複送信のリスクをなくせます。
従来型のSaaSプロダクトを構築する場合でも、自律型のAIエージェントを構築する場合でも、メールを重要な副作用として扱うことで、システムの信頼性を保ち、ユーザーの満足を維持できます。こうした複雑さに対応する開発者ファーストのメールAPIについては、https://sendhq.cc をご覧ください。