api-idempotency-retry について
api-idempotency-retryは、APIチームがサーバー側の冪等キーとクライアントのリトライ動作を一体で設計するためのスキルです。キー契約、原子的な確保、応答の再生、保持期間、SDKの既定動作を扱います。
主な機能
- 01リトライに安全でないエンドポイントの特定
- 02キーの範囲とリクエスト本文の照合
- 03再生期間と保持期間の設定
- 04冪等キーの原子的な確保
- 05処理中と完了後の応答仕様
- 06リトライ指針とSDK既定値の文書化
活用シーン
- 01接続切断後の処理重複の防止
- 02外部HTTP連携向けリトライの設計
- 03APIエンドポイント間の冪等動作の統一
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ developer-platform-skills --skill api-idempotency-retryインストール後、Claude Code で/api-idempotency-retryを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/backoff-jitter-formulas.mdkey-lifecycle-walkthrough.mdvendor-sdk-retry-defaults.md詳細
API の冪等性と再試行
この保証を構成する2つの要素を設計します。再試行された書き込みを安全にするサーバー側の冪等性キーと、そのキーを適切に利用するクライアント側の再試行動作です。どちらか一方だけを実装するのは落とし穴です。Resilience4j のドキュメントにも、これは提案ではなく厳守すべき制約として記されています。再試行する操作は冪等でなければならないため、キーをサポートせずに再試行を案内したり、その逆を行ったりすると、接続が切断されたときにまさに保証が成り立たなくなります。
以下のすべてを理解するうえで、2つの事実が前提となります。
- 信頼性の低いネットワーク上で厳密に一度だけ配信することは不可能です(Tyler Treat による定説となった論証は、FLP 結果に基づいています)。キーによって実現できるのは、あくまで 実質的に一度だけ です。つまり、少なくとも一度の配信と重複の排除を組み合わせ、冪等性を実際に実装した範囲に限定するものです。
Idempotency-Keyヘッダーには、批准済みの標準はありません。IETF のドラフト(draft-ietf-httpapi-idempotency-key-header)は、2026年4月に批准されないまま失効しました。Stripe が普及させたため、ヘッダー名は業界全体で一貫していますが、セマンティクス(TTL、不一致時の動作、並行処理の扱い)はプロバイダーごとに異なります。そのため、独自の仕様を設計して文書化する必要があり、既定のものとして想定してはいけません。
メモ(推奨):メモリをファイルに保存する場合は、developer-platform-context.md の使用を検討してください。別のメモリシステムを使用している場合は、そちらを利用してください。このファイルは参考資料であり、必須要件ではありません。タスク情報はセクションを分けて記録してください。完了したタスクは削除してください。タスクには日付を記載し、プロジェクト全般のコンテキストには日付を記載しないでください。面接での回答は、2つのタスク間で異なる場合があります。
確認事項
設計を始める前に、以下を確認してください。回答によって後続の手順が変わります。まとめて質問してください。これは戦術的な設計タスクであり、戦略面の面接ではありません。
- パラダイム:REST、gRPC、または両方ですか?(gRPC はデッドラインの伝播をネイティブにサポートします。REST ではヘッダー規約と明示的なデッドラインの受け渡しが必要です)
- 冪等ではないエンドポイントはどれですか?エンドポイント一覧または OpenAPI 仕様を入手し、状態を変更する操作をすべて列挙してください。
- キーストアのバックエンドには何を使用しますか?一意制約とトランザクションを備えたリレーショナルデータベース、条件付き書き込みに対応した KV ストア、またはキャッシュのみですか?(原子的な確保の選択肢が使えるかどうかが決まります。手順4を参照してください)
- リクエストの処理中に、外部のロールバック不可能なシステムを呼び出すことはありますか?(課金処理やメール送信など)(復旧ポイントに影響します。手順5を参照してください)
- 提供する SDK の言語は何ですか?また、インテグレーターは生の HTTP を直接呼び出しますか?(再試行の仕組みにどこまで既定の動作を提供できるかが決まります。手順6を参照してください)
- 再試行チェーンが取り得る最長期間はどれくらいですか?デッドレターキューの再処理期間、異議申し立て期間、スケジュールされた再実行などを確認してください。(TTL に影響します。手順3を参照してください)
- この保証はいつまでに必要ですか?チケットを寄せている1社のクライアントで発生する重複課金を止めるための単発の修正ですか、それとも、今後のすべてのエンドポイントが継承する積み重なる契約ですか?キーの確保機構にかけられる工数の上限はどれくらいですか?スキーマや分離レベルの移行が間に合わない納期ですか、それとも SERIALIZABLE ワークロードにすでに習熟していますか?(確保方式の優先順位が変わります。手順4を参照してください。厳しい納期がある場合は、現在のストアがすでにサポートする方式を優先します。長期的に契約として運用する場合は、より充実した確保ライフサイクルを優先します。スキーマ移行ができないという上限がある場合は、到達できない方式を最下位にするのではなく、選択肢から除外します)
誰がリトライループを制御するか
リトライの判断は、製品を購入した開発者またはプログラムが行います。リトライループを誰が制御するかによって、設計は次のように分かれます。
- ファーストパーティの開発者 - 両端を自分で制御します。社内の規約とコードレビューで、その大半に対処できます。最も低コストで対応できる利用者層です。
- 生の HTTP を呼び出すサードパーティーのインテグレーター - ドキュメントだけを頼りに、独自のリトライループを実装します。彼らのための標準が不足を補ってくれないため、キーのセマンティクス、TTL、不一致時および処理中の挙動、バックオフの実例を含む、完全な契約を公開する必要があります。
- SDK を介して呼び出す利用者 - SDK が利用者のリトライループそのものです。デフォルト設定(バックオフ、ジッター、試行回数の上限、キーの自動生成)は、ほとんどの利用者が読み進めることのない契約であるため、デフォルトを誤ると、フリート全体で重複が発生します。
まず生の HTTP を使うインテグレーター向けに設計してください。彼らにとって十分に完全な契約であれば、自社の SDK でも自動化できます。また、SDK のデフォルト設定は、ドキュメントを読まない大多数の利用者の挙動を決めるため、最も効果の高い成果物として扱ってください。
ワークフロー
- リトライに安全でないエンドポイントを特定します。
- キーの契約を設計します。
- ストレージと TTL を設定します。
- キーをアトミックに確保します。
- レスポンスのセマンティクスを設計します。
- クライアント向けのリトライガイダンスと SDK のデフォルト設定を作成します。
- インテグレーター向けに契約を文書化します。
各手順については、以下の各セクションで順に説明します。
1. リトライに安全でないエンドポイントを特定する
- このトピックに関連する唯一の批准済み文書である RFC 9110 では、GET、PUT、DELETE を冪等なメソッド、POST を非冪等なメソッドと定義しています。
Idempotency-Keyを後付けするパターンについては何も述べていません。 - 状態を変更するすべてのエンドポイントを列挙します。それぞれで冪等性キーを受け付けるか、リトライに安全でないことを明示的に文書化する必要があります。これは、レビュー段階のチェック
samber/developer-platform-skills@public-api-design-reviewが実施する内容であり、このスキルは前者を実現するために必要なものを構築します。 - 主張されている冪等性を検証し、メソッドを鵜呑みにしないでください。ログに追記する PUT や、カウンターを減算する DELETE は、メソッドの仕様にかかわらず冪等ではありません。
2. キーの契約を設計する
- キーを生成するのはクライアントであり、サーバーが導出することはありません。AWS の Builders' Library では、却下された代替案を次のように説明しています。リクエストパラメーターをサーバー側でハッシュすると、本当に同一のリクエストを意図的に2つ送る場合(同一のインスタンスを意図的に2つ起動する場合など)に問題が起きます。呼び出し元が指定するキーは、推測されたキーでは表現できない意図を表します。Stripe、AWS、Twilio、Resend はいずれも、キーの決定をクライアントに委ねています。
- キーは呼び出し元ごとにスコープを設定し、
(account_id, idempotency_key)の組み合わせを使います。グローバルには設定しません。Stripe、AWS、標準の Postgres リファレンス実装はいずれもこのペアでキーを管理するため、異なるアカウントで同じキー文字列を再利用しても衝突しません。 - クライアントにはキーの導出方法を推奨してください。どの方法でも機能しますが、単に列挙するのではなく、クライアント向けに順位を付けます(これは Resend 自身が推奨する順序です)。
- 価値:
event-derived > request-scoped > random UUID reused across retries - 労力:
event-derived > request-scoped > random UUID reused across retries - 効率:
event-derived > request-scoped > random UUID reused across retries。価値と労力の順位はここでは同じなので、その差が比率を決めます。労力の差は数分程度です(呼び出し元がすでに持っている識別子をキーに付けるだけです)。それに対して、価値の差は保証全体に及びます。 - デフォルトの方法:イベントから導出(
order-confirm-${orderId})。構造上決定的であり、リトライ時にキーの再利用を確実に避けられます。呼び出し時に安定した業務識別子がない場合は、リクエスト単位の方法に切り替えます。呼び出し元がステートレスなスクリプトの場合は、再試行時に再利用するランダム UUID に切り替えます。どの方法も負担は変わりません。クライアントコード1行で実装できます。 - これらより下位に4つ目の方法を設けてはいけません。試行ごとに新しい値を生成する方法(
Date.now()、リトライ時に再生成されるrandomUUID())は、この仕組みを完全に無効化するため、アンチパターンとして記載してください。
- 価値:
- ペイロードを検証します。同じキーで異なるリクエストボディが送られた場合はクライアントのバグです。拒否してください(一般的には 409 が使われます。Resend では
409 invalid_idempotent_requestとしています)。元の結果を黙って再生してはいけません。期限切れの IETF ドラフトでは、この考え方を拡張し、キーと一緒に保存する任意のペイロード フィンガープリント を定義しています。最も簡単な形式は、ペイロードのチェックサムです。 - 成功と失敗の両方を保存します。Stripe では 500 を含む失敗も保存し、リトライ時に元の結果をそのまま再生します。保存した失敗をそのまま再生するのか(Stripe v1)、副作用なしで操作を再試行できるようにするのか(Stripe v2)を明示的に決めてください。未決定のままにしておくと、設計上の欠落になります。
3. ストレージと TTL を設定する
- 保持期間は、単一の TTL ではなく、別々の2つの要件として扱います(Brandur Leach のリファレンス実装)。
- 再生期間(約24時間):冪等な結果再生の正しさを保つ期間です。Stripe、Resend、Twilio で文書化されている一般的な慣例でもあります。
- 削除処理のしきい値(約72時間):人が復旧できるよう、レコードをより長く保持する運用上の期間です。金曜日のデプロイ失敗で発生したリクエストを週末のあいだ残し、月曜日に修正できるようにします。
- 再生期間は、最長のリトライチェーン(質問6)を基準に設定し、ストレージコストを基準にしてはいけません。7日間の DLQ 再生に対して TTL が24時間では、重複が起きるのを待つようなものです。AWS は EC2 の実行トークンを、リソースの存続期間に猶予期間を加えた期間保持します。そのため、リソースが削除された後に遅れて届いたリトライにも、一貫した応答を返せます。
- 期限切れ後に同じキーが再利用された場合は、新しいリクエストとして扱います。調査したプロバイダーはすべてこの方法を採用しており、TTL 経過後のキー再利用に対してエラーを返すプロバイダーはありません。
- 要約ではなく、完全なレスポンス(ステータス、重要なヘッダー、ボディ)をそのまま保存します。再生されたレスポンスは元のレスポンスと区別できないものでなければなりません。
4. キーをアトミックに確保する
確認してから挿入する方式は、同時リトライ時に TOCTOU 競合を引き起こします。両方の呼び出し元が「存在しない」と判断し、どちらも処理を進めてしまいます。確保は、ストレージ層が調停する単一のアトミック操作で行う必要があります。本番環境では、次の 3 つの方式が使われています。
-
効率性:
DB unique constraint > conditional-write OCC > row lock + SERIALIZABLE -
導入の手間:
row lock + SERIALIZABLE > conditional-write OCC > DB unique constraint -
価値:
row lock + SERIALIZABLE > DB unique constraint == conditional-write OCC -
デフォルトの選択肢:
(account_id, idempotency_key)に対する一意制約。 データベースが単一の勝者を決めます。敗者はアプリケーションレベルのチェックではなく、INSERTの失敗自体によって 409 を受け取ります。ほぼコストはかかりません。インデックス 1 つで済み、トランザクション分離によって、以下のフェンシング保証も追加コストなしで得られます。 -
**条件付き書き込み OCC(DynamoDB 方式)**は、一意制約を使ったトランザクションに対応していないストアでも、同じ単一勝者の確保を実現します。価値の項目で同点なのは実際のところです。同じ保証を、異なるストレージファミリーで実現します。質問 3 への回答によって、2 つのうちどちらかが選択肢から外れます。除外された選択肢は順位を下げるのではなく、削除します。
-
行ロック + SERIALIZABLE トランザクションは、評価が伸び悩む選択肢です。
- 価値:SERIALIZABLE トランザクション内の
locked_at列によって、処理中 を第一級の状態として表現できます。ロックの期限切れ後も状態が保持され、再開可能な復旧ポイントの基盤になります。 - 導入の手間:分離レベルの調整や、シリアライズ失敗時のリトライ処理が必要です。そのため、効率性を評価する各ラウンドで劣ります。
- リクエストが外部の状態変更を伴う場合(質問 4)は、この方式を選びましょう。複数ステップにまたがるリクエストには、勝者を決めるだけでなく、より豊かな確保のライフサイクルが必要です。
- 価値:SERIALIZABLE トランザクション内の
-
どの方式でも、確保とビジネス上の変更は、1 つの ACID 単位としてコミットする必要があります。AWS はこれをベストプラクティスではなく、必須要件として明示しています。保護対象のトランザクションの外で確保を行うと、実際には作成されなかったリソースのトークンが記録されたり、その逆が起きたりする可能性があります。
-
単純な Redis の
SETNX+ TTL ロックは、4 つ目の選択肢ではありません。Kleppmann の分析によれば、フェンシングトークンがなければ、リースの期限切れを過ぎても処理が停止していたクライアントが、ロックの所有者が変わった後に書き込みを行う可能性があります。タイミングに関する前提を置いても解決しません。調査対象のベンダーに、確保に分散ロックを使っているところはありません。Redis しか使えない場合は、フェンシングトークンを重ねて実装してください。一意制約を使えば、この問題を完全に回避できることも覚えておきましょう。 -
この順位付けはデフォルトであり、絶対的なルールではありません。ストレージに関する回答と、導入の手間の上限(質問 3 と 7)に照らして順位を見直してください。すでに SERIALIZABLE のワークロードを実行しているチームなら、導入の手間の項目で想定されているよりも、評価が伸び悩む選択肢をはるかに低コストで採用できます。スキーマ移行が許されないリリース日程なら、現在のストアがすでにサポートしている方式が優先されます。
5. レスポンスの意味を設計する
自前実装でよくあるバグは、キーを「処理済み」か「未処理」かの二択として扱うことです。キーは 未登録、処理中、完了 の3つの状態を持つステートマシンです。処理中のキーに対するリトライはバグではありません(「接続が切れたときに、クライアントライブラリは設定どおりに動作した」のです)。バグは、「認識済みだが未完了」と伝える方法がないサーバーにあります。
| 結果 | 発生したこと | 正しい動作 |
|---|---|---|
| 一度も受信していない | 処理前にネットワークが切断 | 最初の試行としてリトライを進め、通常どおりクレームを取得します |
| 受信したが未処理 | クレームを保持しているが、まだ終端結果がない | 処理中の競合を返すか、元のロックの期限切れ後に最後の復旧ポイントから再開します |
| 処理済みだが応答を喪失 | 結果はコミット済みだが、応答が届かなかった | 保存済みの応答をそのまま再送します。再実行はしません |
-
処理中の場合は、次のいずれかを選びます。
- 409 で拒否します(Resend の
409 concurrent_idempotent_requests。ドキュメントに記載されたアドバイスは「待ってから再試行」)。最もシンプルで安全なデフォルトです。 - 呼び出し元が同期的に結果を必要とする場合は、結果が得られるまでブロックします。
- 時間のかかる処理には、202 とステータス URL を返します。
最初の呼び出し元が「停止しているように見える」からといって、2番目の呼び出し元を決して処理に進ませてはいけません。失効した IETF ドラフトでは、処理中の競合と完了済みリプレイを、互いの代替ではなく、意図的に異なる応答として扱っていました。
- 409 で拒否します(Resend の
-
複数ステップのリクエスト(質問4)の場合は、他システムの状態を変更する処理ごとに区切ったアトミックなフェーズに分解し、フェーズ間で復旧ポイントを永続化します。これにより、リトライされたリクエストは、すでに実行した外部呼び出しを再実行せず、正確に中断した箇所から再開します。呼び出しを行う前に実行意図を記録し、呼び出し後、応答を受け取る前にクラッシュしても証跡が残るようにします。
-
受動的な期限切れ処理ではなく、アクティブなバックグラウンドジョブとして復旧を実行します(Brandur のモデル)。
- completer は、放置された処理中リクエストを完了まで進めます。
- reaper は期限切れのキーを削除し、完了できなかった処理を人が確認できるように記録します。
-
エラー応答の形式(コード、エンベロープ、
retryableフラグ、Retry-After)はsamber/developer-platform-skills@api-error-designの領域です。ここで再設計せず、そのシグナルを利用してください。2種類の 409 の意味(ペイロードの不一致と処理中)を、機械可読な異なるコードで区別するのは、このスキルの状態にそのスキルの分類体系を適用することです。
完全なライフサイクルの解説(スキーマ、クレームクエリ、3つの状態すべての応答、複数ステップの復旧ポイントの例)については、references/key-lifecycle-walkthrough.md を参照してください。
6. クライアントのリトライ指針と SDK のデフォルトを記述する
- 明示的な許可リストにある場合のみリトライし、無条件の例外捕捉は避けます。対象は 429(
Retry-Afterに従う)、502、503、504、接続タイムアウト、接続リセットです。500 は慎重に扱います(500 の中には、同じリクエストを再試行しても解決しない実際のバグが原因のものがあります)。429 以外の 4xx、認証エラー、バグに起因する例外はリトライしません。独立した 4 つの情報源(tenacity、Resilience4j、Resend、レスポンスを返す側)が、いずれもこの厳密な区分に収束しています。特定ベンダーの意見ではなく、実践例から導かれた共通の知見として引用してください。 - すでに冪等性キーが設定されていない限り、冪等でない操作で読み取りタイムアウトが発生してもリトライしてはいけません。これは、このスキルのサーバー側が解消するための境界条件そのものです。
- バックオフにはジッターを加える必要があります。順位(Marc Brooker による AWS の分析が標準的な出典です):
- 価値:
full jitter > decorrelated jitter > equal jitter > no jitter - 労力:
full jitter == decorrelated jitter == equal jitter == no jitter。これは実際に同率であり、はぐらかしではありません。いずれも数式 1 つ、またはライブラリのフラグ 1 つで、記述、テスト、逆算の手間は同じです。そのため、順位の上位にある方式が、読者の時間を余計に使うことで価値を得るわけではありません。 - 効率:労力が同じなので、価値の順位は変わりません。
- デフォルトの順位:full jitter(
sleep = random(0, min(cap, base * 2^attempt)))。クライアント全体の処理量とサーバー負荷が最も小さくなる代わりに、完了までの総時間はやや長くなります。 - この順位で次点となるのは decorrelated jitter です。処理量は増えますが、より速く完了します。ユーザー向けの呼び出しが厳しい期限の中で実行され、システム全体の負荷より成功までの時間を重視する場合に昇格させます。Equal jitter は、ジッターありの方式の中で Brooker が「明確な敗者」と呼ぶ方式です。ジッターなしでは、障害の原因となったスパイクとまったく同じ状態にクライアント全体が再び同期します。インテグレーターに提示する選択肢に、どちらも含めないでください。
- 呼び出し元について把握している情報に基づいて順位を付け直します。単一インスタンスで実行されるバッチジョブでは、この順位が想定するクライアント間の同期によるコストは発生しません。
- 価値:
- 試行回数だけでなく、試行回数と総実行時間の両方に上限を設けます。各試行が妥当な時間内でも、ループ全体では呼び出し元自身の予算を超える場合があります。リトライのたびに(試行回数、エラー、次の待ち時間)をログに記録します。ログに残らないリトライループは、発生しかけている障害を見えなくします。
- 呼び出し元ごとの上限とは別に、システム全体のリトライ予算を設けます。一定期間内のトラフィックに対してリトライが所定の割合を超えたら、それ以上のリトライを停止します。100 個のインスタンスがそれぞれ 3 回リトライすると、個々のインスタンスでは妥当に見えても負荷は 300 倍に増幅します。AWS のトークンバケットは、実際の障害時により早くクォータを適用するよう調整された実装として知られています。過渡エラーのリトライでは 14 トークン、スロットリングのリトライでは 5 トークンを使用します。
- 期限を伝播させます。最初に全体の期限を 1 つ設定し、その範囲内で試行ごとのタイムアウトを割り当てます。試行ごとに新たな全体タイムアウトを設定してはいけません。これにより、呼び出し元が実際に待つ時間が気づかないうちに延長されます。単一のタイムアウトは、OS のデフォルト値ではなく、測定した p99 の 2~3 倍に設定します。チェーンでの仕組みを説明する際に引用できるのは、gRPC のワイヤー上で相対タイムアウトを扱うモデルです。
- 数式を説明する前に、インテグレーターには使用言語のリトライライブラリ(tenacity、Resilience4j、または同等のもの)を案内します。ライブラリを使えば、数式は 1 行で済み、デバッグも済んでいます。サーキットブレーカーと組み合わせる場合は、適用順序を明記します:
Retry → CircuitBreaker → RateLimiter → Bulkhead。 - 提供する SDK では、バックオフとジッターを有効にし、試行回数に上限を設けたリトライをデフォルトで有効にします(AWS のデフォルトは合計 3 回です)。また、SDK で可能な場合は、安全でない操作に冪等性キーを自動生成します。ドキュメントを読まない呼び出し元にとって、デフォルト設定が契約となります。リトライのデフォルト設定とプラットフォームのレート制限ポリシーとの相互作用(SDK のリトライがクォータに含まれるか、適応型スロットリングを行うかなど)は、
samber/developer-platform-skills@api-rate-limit-policyの対象です。
数式、予算の仕組み、期限の計算については、references/backoff-jitter-formulas.md を参照してください。デフォルト設定を調整するために比較する各ベンダーの設定については、references/vendor-sdk-retry-defaults.md を参照してください。
7. インテグレーター向けに契約を文書化する
意味を定義する標準が存在しないため、インテグレーターにとって唯一の規範的な情報源は、あなたのドキュメントです。上記の対象者の区分に応じて、次の事項を文書化してください。
- キーを受け付けるエンドポイント、ヘッダー名、キーの最大長、推奨される導出方法。
- スコープ、具体的な数値で示した再生ウィンドウ、および期限切れ後の動作(「新しいリクエストとして扱う」)。
- 2 種類の競合時の動作と、それぞれ異なるエラーコード(ペイロードの不一致と処理中の重複)。
- 保存された失敗を再生するのか、再試行するのか(Stripe の v1/v2 の違いを、自分の API に適用したもの)。
- 具体的な数値を使った再試行ループの例(スケジュール、上限、ジッター)。Resend のドキュメントにある
1s → 2s → 4s → 8s、最大 30s の例のようなものです。 - 「exactly-once delivery(厳密に 1 回の配信)」とは決して書かないでください。オープンネットワークでは、この主張が誤りであることは証明可能です。代わりに、事実に即して、文書化したキーのスコープとウィンドウ内では再試行が安全であると記載してください。マーケティング資料で既にこの主張をしている場合は、該当箇所をすべて修正してください(Treat の最も有力な主張は、Kafka の exactly-once は閉じたトランザクションシステム内でのみ成立し、パブリック API は定義上それに該当しない、というものです)。
障害モード
アンチパターンのチェックリスト(いずれも直接的な監査指摘事項です)。
- キーを受け付けても、保存も適用もしないこと。安全に再試行できるとクライアントに誤認させるため、キーを受け付けない場合よりも悪い状態です。
- 試行ごとに新しいキー値を生成すること。
- TOCTOU 競合が発生する check-then-insert 方式のクレーム処理。
- 処理中の状態を表現できない二値のキー状態。
- 同じキーでペイロードが異なる場合に、黙って再生すること。
- 最長の再試行チェーンより再生ウィンドウが短いこと(24 時間 TTL の背後に 7 日 DLQ がある落とし穴)。
- クレームとしてフェンシングトークンのない単純な
SETNXロックを使うこと。 - 保存された失敗を再生するのか、再実行するのか決めていないこと。
- 試行ごとに全体のタイムアウトをリセットする再試行ループ。
- ジッターのないバックオフ。
- ドキュメント内のあらゆる箇所で「Exactly-once」と記載すること。
測定
- 並行実行ゲート:状態を変更するエンドポイントに対して、1 つのキーを使い、同一のリクエストを N 件同時に送信します。副作用は 1 回だけ実行され、残りの N-1 件には処理中の競合または保存済みの再生結果が返される必要があります。合格か不合格かの二択で判定し、成立するまでクレーム設計を繰り返し改善してください。これは定義上の保証であり、直接テストできます。
- クラッシュ復旧ゲート:複数ステップのリクエストにおけるアトミックなフェーズの間でサーバーを停止し、その後リクエストを再試行します。リクエストは復旧ポイントから再開し、外部呼び出しが一切繰り返されない必要があります。設問 4 に「はい」と回答した場合に合格か不合格かの二択で判定します。
- 本番環境での重複した副作用の発生率:文書化したスコープとウィンドウ内では、設計目標は定義上ゼロです。調整可能なしきい値ではなく、ゲートが後退したことを示すアラームとして追跡してください。
- 孤立キーの発生率(クレームされたが完了しなかったキー。完了処理キュー):業界で公表されているしきい値は存在しません。情報源のない運用メトリクスと同様、最初の 1 か月の実績から基準値を設定し、傾向を監視してください。
呼び出し例
- 「POST /payments と POST /orders のエンドポイントに Idempotency-Key のサポートを追加してください。モバイルクライアントがタイムアウト時に再試行すると、二重課金が発生しています。」
- 「API ドキュメントの再試行ガイダンスのセクションを作成し、提供する Python SDK と Node SDK のバックオフのデフォルト値を選んでください。」
- 「並行して動作した 2 つのウェブフックハンドラーが、同じ請求書を作成しました。冪等性の実装を監査し、競合を見つけてください。」
参考資料
- references/key-lifecycle-walkthrough.md - キーのライフサイクルの具体例:スキーマ、アトミックなクレーム、実際の 409 コードを使った 3 状態の応答、復旧ポイント、完了処理/再取得処理。
- references/backoff-jitter-formulas.md - 3 種類のジッター計算式とトレードオフ、再試行予算の仕組み、デッドラインの伝播、タイムアウトの設定。
- references/vendor-sdk-retry-defaults.md - AWS、Stripe、Google Cloud、Twilio、Resend における再試行と冪等性のデフォルト動作、参考にすべき点と注意を促すべき点。