べき等キー
API リクエストの重複実行を防ぐために、クライアントが付与する一意な識別子
べき等キーとは
べき等キー (Idempotency Key) は、API リクエストに付与する一意な識別子 (UUID) で、同じリクエストが複数回送信されても 1 回だけ処理されることを保証する仕組みである。ネットワークタイムアウトでクライアントがリトライした場合に、二重課金や二重注文を防ぐ。
Stripe が決済 API で Idempotency-Key ヘッダーを採用したことで広く知られるようになった。現在では決済に限らず、副作用を持つ POST/PUT リクエスト全般で使われるパターンだ。
なぜ必要か
分散システムでは、リクエストが「成功したが応答が返らなかった」状況が頻繁に発生する。
クライアント → POST /payments → サーバー (決済成功)
クライアント ← タイムアウト ← ネットワーク障害
クライアント → POST /payments → サーバー (???)
べき等キーがなければ、サーバーは 2 回目のリクエストを新規として処理し、二重課金が発生する。べき等キーがあれば、サーバーは「このキーは処理済み」と判断し、最初の結果をキャッシュから返す。
実装パターン
// クライアント: リクエストにべき等キーを付与
const idempotencyKey = crypto.randomUUID();
const response = await fetch('/api/payments', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey },
body: JSON.stringify({ amount: 1000, currency: 'JPY' }),
});
// リトライ時は同じキーを使う
if (!response.ok) {
const retry = await fetch('/api/payments', {
method: 'POST',
headers: { 'Idempotency-Key': idempotencyKey }, // 同じキー
body: JSON.stringify({ amount: 1000, currency: 'JPY' }),
});
}
DynamoDB での実装
DynamoDB の条件付き書き込みで、べき等キーの重複を原子的にチェックできる。
async function processPayment(event: APIGatewayEvent) {
const key = event.headers['idempotency-key'];
if (!key) return { statusCode: 400, body: 'Idempotency-Key required' };
// 1. 既存の結果を確認
const existing = await ddb.send(new GetCommand({
TableName: TABLE_NAME,
Key: { pk: `IDEMP#${key}` },
}));
if (existing.Item) return JSON.parse(existing.Item.response);
// 2. 処理を実行
const result = await chargePayment(JSON.parse(event.body));
// 3. 結果を保存 (条件付き書き込みで競合を防止)
await ddb.send(new PutCommand({
TableName: TABLE_NAME,
Item: {
pk: `IDEMP#${key}`,
response: JSON.stringify(result),
ttl: Math.floor(Date.now() / 1000) + 86400, // 24 時間後に自動削除
},
ConditionExpression: 'attribute_not_exists(pk)',
}));
return result;
}
ConditionExpression: 'attribute_not_exists(pk)' により、2 つのリクエストが同時に到達しても、1 つだけが書き込みに成功する。失敗した方は ConditionalCheckFailedException を受け取り、既存の結果を返す。
設計上の考慮点
TTL の設定
べき等キーのレコードを永久に保持するとストレージが膨張する。Stripe は 24 時間、多くの実装では 24〜72 時間の TTL を設定する。TTL 経過後に同じキーでリクエストすると、新規として処理される。
キーの生成はクライアントの責務
べき等キーはクライアントが生成する。サーバーが生成すると、リトライ時に「前回のキーが何だったか」をクライアントが知る手段がない。UUID v4 が一般的だが、ビジネスロジックに基づくキー (注文 ID + 操作種別) を使うこともある。
進行中のリクエストの扱い
最初のリクエストが処理中に 2 回目のリクエストが到達した場合、409 Conflict を返して「処理中」であることを伝える。クライアントは一定時間後にリトライする。
Lambda Powertools の Idempotency
AWS Lambda Powertools には Idempotency ユーティリティが組み込まれており、DynamoDB ベースのべき等性を数行で実装できる。
冪等性キーの実装方法
| 方法 | ストレージ | TTL |
|---|---|---|
| DynamoDB | 条件付き書き込み | TTL で自動削除 |
| Redis | SET NX EX | 自動期限切れ |
| RDB | UNIQUE 制約 | 手動削除 |
全体像を把握するには関連書籍も有用。