リトライパターン

一時的な障害だけを選んで再試行し、間隔を指数的に広げつつ乱数で散らして、回復途中の相手に負荷を集中させない設計パターン

耐障害性分散システム

リトライパターンとは

リトライパターンは、ネットワークタイムアウト、サービスの一時的な過負荷、データベースのロック競合など、一時的な障害 (Transient Fault) に対して自動的にリクエストを再試行する設計パターンである。分散システムでは一時的な障害が日常的に起きるため、ほとんどの外部呼び出しにリトライが必要になる。ただしリトライは無料ではない。障害中のサービスから見ればリトライは追加の負荷であり、打ち方を間違えると相手の回復を遅らせる。しかも呼び出しが多段になっていると各層のリトライは掛け算で効く。クライアント・API ゲートウェイ・バックエンドがそれぞれ最大 3 回試行する設計なら、最下層には 1 件のリクエストが最大 27 回届く。リトライを入れるときは、どの層で打つか (原則は 1 箇所) といつ諦めるかを先に決める。AWS SDK の標準リトライモードはリトライ用のトークンバケットを持ち、リトライが続いて枠を使い切ると以降はリトライせずエラーを返す (2026 年 8 月時点)。こうした予算を持たない自前実装では、諦める条件を自分で決める必要がある。

指数バックオフ + ジッター

リトライ間隔を固定 (1 秒ごと) にすると、障害中のサービスにリクエストが集中し、回復を妨げる (Thundering Herd)。指数バックオフはリトライごとに間隔を倍増させてこの圧力を下げるが、これだけでは足りない。同じ瞬間に失敗したクライアントは同じ計算式で同じ待ち時間を得るため、間隔が伸びても足並みは揃ったままで、山が後ろにずれるだけになる。ジッターはこの同期を崩すために入れる。

AWS SDK の標準リトライモードが使うのは full jitter と呼ばれる形で、待ち時間は 0 から上限までの一様乱数になる。上限は min(20,000 ms, 基準値 × 2^リトライ回数) で、基準値は一時的なエラーが 50 ms・スロットリングが 1,000 ms である (2026 年 8 月時点)。倍増を打ち切る上限を必ず置く点も重要で、これが無いと数回の失敗で待ち時間が分単位に膨らむ。

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000,
  maxDelay = 20000,
): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await fn();
    } catch (err) {
      if (attempt === maxRetries || !isRetryable(err)) throw err;

      // full jitter: 上限を指数で伸ばし、0 からその上限までの一様乱数だけ待つ
      const cap = Math.min(maxDelay, baseDelay * 2 ** attempt);
      await new Promise(r => setTimeout(r, Math.random() * cap));
    }
  }
}

const RETRYABLE_STATUS = new Set([408, 429, 500, 502, 503, 504]);
const RETRYABLE_CODE = new Set(['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT']);

function isRetryable(err: unknown): boolean {
  const e = (err ?? {}) as { statusCode?: unknown; code?: unknown };
  if (typeof e.statusCode === 'number') return RETRYABLE_STATUS.has(e.statusCode);
  // 状態コードを持たない例外は、接続の失敗と分かるものだけ再試行する
  return typeof e.code === 'string' && RETRYABLE_CODE.has(e.code);
}
リトライ 1: 0〜1  (上限 1,000 ms × 2^0)
リトライ 2: 0〜2  (上限 1,000 ms × 2^1)
リトライ 3: 0〜4  (上限 1,000 ms × 2^2)

待ち時間の期待値は上限の半分になるので、同期を崩しながら平均の待ち時間はむしろ短くなる。 判定関数をリトライ可の条件を列挙する向きで書いているのも意図的である。 逆向き (分からないものはリトライする) にすると、値の取り違えのようなプログラムの誤りまで上限回数まで再試行してしまう。

リトライすべきケースとすべきでないケース

判断の軸は、時間が経てば直る見込みがあるかの一点である。相手が過負荷で断ったのなら待てば直るが、リクエストの中身が間違っているなら何度送っても同じ答えが返る。状態コードの数値範囲だけで割り切らないこと。5xx を一括でリトライ可とする実装は多いが、501 Not Implemented のように待っても直らない 5xx もある。

リトライすべき (一時的)リトライすべきでない (恒久的)
HTTP 429 (Too Many Requests)HTTP 400 (Bad Request)
HTTP 503 (Service Unavailable)HTTP 401 (Unauthorized)
ネットワークタイムアウトHTTP 404 (Not Found)
DB のロック競合バリデーションエラー
DynamoDB の ProvisionedThroughputExceededExceptionビジネスロジックエラー

恒久的なエラーをリトライしても成功しない。リソースを無駄に消費し、障害の回復を遅らせる。判断が難しいのはタイムアウトで、応答が返らなかったという事実は相手が処理しなかった証拠にはならない。処理が完了した直後に応答だけが失われた可能性が常に残るため、タイムアウトを一時的な障害として再試行できるのは、その操作がべき等な場合だけである。

AWS での実装パターン

SQS + Lambda

Lambda がエラーを返すと、そのメッセージは削除されずに可視性タイムアウトの経過後にキューで再び可視になり、Lambda のポーラーが受信し直す。再送を止める仕組みはキュー側のリドライブポリシーの maxReceiveCount で、これはリトライ回数ではなく受信回数の上限である。メッセージの受信回数がこの値を超えるとデッドレターキュー (DLQ) へ移されるため、3 を指定した場合の再試行は 2 回になる。DLQ を設定する場所も Lambda 側ではなくキュー側である。

既定ではバッチ単位で戻る点も落とし穴になる。1 件の処理で失敗すると、成功していたメッセージまで含めてバッチ全体が再び可視になり、同じメッセージが何度も処理される。イベントソースマッピングの FunctionResponseTypesReportBatchItemFailures を指定し、失敗したメッセージ ID だけを batchItemFailures で返す形にすれば、再送を失敗分に絞れる (2026 年 8 月時点)。ただし関数が例外を投げた場合はバッチ全体の失敗として扱われるため、この形にするなら例外を捕まえて返す実装に変える必要がある。

Step Functions

Step FunctionsRetry フィールドで、エラー名ごとにリトライ戦略を定義できる。既定値は IntervalSeconds が 1 秒・MaxAttempts が 3・BackoffRate が 2.0 で、MaxAttempts は合計試行回数ではなくリトライ回数を指す (2026 年 8 月時点)。

{
  "Retry": [{
    "ErrorEquals": [
      "Lambda.ServiceException",
      "Lambda.AWSLambdaException",
      "Lambda.SdkClientException",
      "Lambda.TooManyRequestsException"
    ],
    "IntervalSeconds": 2,
    "MaxAttempts": 3,
    "BackoffRate": 2.0,
    "MaxDelaySeconds": 20,
    "JitterStrategy": "FULL"
  }]
}

エラー名を個別に挙げているのは、States.TaskFailedStates.Timeout 以外の既知のエラー名すべてに一致するワイルドカードだからである。 これを指定すると、入力の不備のような待っても直らない失敗まで拾ってリトライしてしまう。 JitterStrategy の既定は NONE で、FULL を明示しない限り揺らぎは入らない。 MaxDelaySeconds を省くと BackoffRate で伸びた待ち時間に上限が無くなる。 リトライは 1 回ごとに状態遷移として課金される点も設計時に見ておく。

リトライとべき等性

リトライが安全に動作するには、操作がべき等 (同じ操作を複数回実行しても結果が同じ) である必要がある。PUT /orders/123 は何度実行しても同じ結果だが、POST /orders は実行するたびに新しい注文が作成される。べき等でない操作にはべき等キーを使う。べき等キーは、受け取った側がキーと処理結果を保存しておき、同じキーの再送には保存済みの応答をそのまま返す仕組みである。キーを発行するのは呼び出す側で、リトライのたびに新しいキーを作ってしまうと二重処理は防げない。

リトライを実装したら、試行回数を必ず計測できるようにする。成功率だけを見ていると、内部で 4 回試行してかろうじて成功している状態を正常と読み違える。試行回数の分布・リトライを使い切った末の失敗率・DLQ の滞留件数を分けて監視すれば、依存先の劣化を利用者に見える障害になる前に捕まえられる。

この記事は役に立ちましたか?

関連用語

関連する記事