べき等キー

API リクエストの重複実行を防ぐために、クライアントが付与する一意な識別子

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 制約 手動削除

全体像を把握するには関連書籍も有用。

関連用語