冪等キー:再試行を安全にする一回の書き込み
ネットワークのタイムアウトはサーバが何もしなかった証拠ではありません。冪等キーで同一リクエストを識別し、再実行せず最初の結果を返します。
クライアントが支払いを送信し、接続がタイムアウトし、自動で再試行する。最初の試行が実は成功していたら、代金は二度動きます。タイムアウトは失敗ではありません。 これがすべての再試行の前提です。
キーがすること
クライアントは業務上の意図ごとに一つの一意なキー(UUID)を生成し、再試行時に同じキーを使います。
POST /payments
Idempotency-Key: 6f4a1e2c-9b3d-4c8a-9f21-7e5d0b3a1c88
サーバの処理はこうです。その値を一意制約とする行を挿入します。最初のリクエストが実際に実行され結果を保存し、以降の同じキーのリクエストは保存済みの結果を返します。
実装の要点
async function handle(key: string, body: Body) {
try {
const row = await db.insert('idempotency', { key, state: 'in_progress' });
} catch (e) {
if (!isUniqueViolation(e)) throw e;
// 既存:保存済み結果を返すか、処理中と伝える
const existing = await db.get('idempotency', key);
if (existing.state === 'done') return existing.response;
return conflict('in progress');
}
const result = await doWork(body);
await db.update('idempotency', key, { state: 'done', response: result });
return result;
}
見落としがちな三点です。
| 要点 | 欠けると |
|---|---|
| データベース層の一意インデックス | 並行要求が両方挿入し、両方実行される |
| 元の応答本文の保存 | 再試行で再計算が必要になり意味を失う |
| in_progress 状態の保持 | 実行中と完了を区別できない |
スコープと有効期限
キーは認証主体とエンドポイントに紐づけます。でなければ、あるユーザーのキーが別のユーザーの記録に当たります。
有効期限は業務判断です。支払いは通常一日から数日保持します。短すぎると本当の再試行を取りこぼし、長すぎると表が無限に育ちます。清掃は業務トランザクションの外で行い、実行中の記録を消さないようにします。
リクエスト本文の指紋
厳格な実装では本文のハッシュも保存し、同じキーで内容が違う誤用を検出します。
if (existing.requestHash !== sha256(body)) {
return unprocessable('key reused with different payload');
}
ここは 409 より 422 が適します。状態の衝突ではなく、クライアントの実装上の誤りです。
不要な場面
もともと冪等な操作にキーは不要です。GET、全体置換の PUT、DELETE。資源を作る POST、金銭を動かすもの、メッセージを送るものには必要です。 判断基準は「二回実行すると副作用が二度起きるか」です。
冪等キーは再試行を安全にするのではなく、「これは同じリクエストだ」とサーバが認識できるようにするものです。

コメント
…