- 技術 💻
指数バックオフとは?リトライ間隔の決め方とジッターの実装
指数バックオフ(Exponential Backoff)とは、処理が失敗したときに、リトライ(再試行)までの待ち時間を「1秒・2秒・4秒・8秒…」と倍々に延ばしていく方法です。実務では、待ち時間に上限を設け、さらにランダムなばらつき(ジッター)を加えて使うのが基本です。
通信エラーや一時的な混雑は、少し待てば回復することが多いため、リトライは有効な対策です。一方で、リトライの仕方を間違えると、障害中のサーバーにアクセスが集中し、回復を遅らせる原因にもなります。
この記事では、指数バックオフの計算式、ジッターの種類と選び方、リトライしてよいエラー・してはいけないエラー、Retry-Afterヘッダー、冪等性、リトライの増幅といった実務の注意点を、PythonとJavaScriptの実装例つきで解説します。
固定間隔のリトライでは、なぜ足りないのか
「失敗したら1秒待ってリトライ」を繰り返す固定間隔の方法には、2つの問題があります。
- 障害が長引くと、無駄なリトライを繰り返す — 数分続く障害でも、1秒ごとに試し続けます
- サーバーの回復を妨げる — 多くのクライアントが同じ間隔でリトライすると、回復しかけたサーバーに再びアクセスが集中します
指数バックオフは、失敗が続くほど間隔を広げることで、短い障害にはすぐ再試行し、長い障害にはサーバーを休ませるという両立を図ります。
計算式と待ち時間の例
待ち時間 = min(上限, 基準時間 × 2 ^ 試行回数)
基準時間1秒、上限30秒の場合、待ち時間は次のようになります。
| 何回目のリトライか | 計算 | 待ち時間 |
|---|---|---|
| 1回目 | 1 × 2⁰ | 1秒 |
| 2回目 | 1 × 2¹ | 2秒 |
| 3回目 | 1 × 2² | 4秒 |
| 4回目 | 1 × 2³ | 8秒 |
| 5回目 | 1 × 2⁴ | 16秒 |
| 6回目 | 1 × 2⁵ = 32 → 上限 | 30秒 |
上限を設けないと、10回目で約8分半、15回目で約4時間半と、待ち時間が実用的でない長さになります。上限は必ず設定します。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「指数バックオフとは?リトライ間隔の決め方とジッターの実装」
ジッター:待ち時間をばらつかせる
指数バックオフだけでは、まだ問題が残ります。同じ時刻に失敗した多数のクライアントは、同じ時刻にリトライするからです。
例えば、サーバーが一瞬止まって1,000台のクライアントが同時に失敗すると、1秒後・2秒後・4秒後に、1,000台が一斉にリトライします。この「一斉に押し寄せる」現象は、サンダリングハード(Thundering Herd)と呼ばれます。
これを防ぐのがジッターで、待ち時間にランダムなばらつきを加え、リトライのタイミングを分散させます。
| 種類 | 計算方法 | 特徴 |
|---|---|---|
| ジッターなし | 待ち時間そのまま | 一斉リトライが起きる |
| Full Jitter | 0〜待ち時間の間でランダム | 分散効果が最も大きい。多くの場面での推奨 |
| Equal Jitter | 待ち時間の半分+0〜半分の間でランダム | 最低限の待ち時間を保証したいとき |
| Decorrelated Jitter | 基準時間〜前回の待ち時間×3の間でランダム(上限あり) | 前回の待ち時間をもとに決める方式 |
AWSのアーキテクチャブログの検証(Exponential Backoff And Jitter)では、ジッターなしに比べて、ジッターありの方式は完了までの総リクエスト数が大幅に少なく、中でもFull Jitterは処理の量と完了時間のバランスが良い結果でした。迷ったらFull Jitterを選ぶのが一般的です。
Full Jitter の待ち時間 = random(0, min(上限, 基準時間 × 2 ^ 試行回数))
リトライしてよいエラー、してはいけないエラー
リトライは、時間をおけば成功する見込みがあるエラーにだけ行います。リクエスト自体に問題があるエラーは、何度送っても同じ結果になります。
| エラー | リトライ | 理由 |
|---|---|---|
| 接続できない・タイムアウト | する(冪等な処理のみ) | 一時的なネットワークの問題の可能性 |
| 408 Request Timeout | する | サーバー側のタイムアウト |
| 429 Too Many Requests | する(Retry-Afterに従う) | 回数制限。待てば回復する |
| 500 Internal Server Error | 状況による | 一時的なこともあるが、恒常的なバグの場合もある |
| 502 Bad Gateway | する | 中継先の一時的な問題 |
| 503 Service Unavailable | する(Retry-Afterに従う) | 過負荷・メンテナンス |
| 504 Gateway Timeout | する(冪等な処理のみ) | 中継先の応答遅れ |
| 400 Bad Request | しない | リクエストの内容が誤っている |
| 401 Unauthorized | しない(トークンを再取得してから1回だけ) | 認証の問題 |
| 403 Forbidden | しない | 権限の問題。何度送っても同じ |
| 404 Not Found | しない | 対象が存在しない |
| 409 Conflict | 状況による | 最新の状態を取得し直してから判断する |
| 422 Unprocessable Content | しない | 入力内容の問題 |
4xx(クライアント側のエラー)は、408と429を除いて基本的にリトライしない、と覚えておくと判断しやすくなります。
Retry-Afterヘッダーを優先する
429や503のレスポンスには、「何秒後に再試行してよいか」を示すRetry-Afterヘッダーが付くことがあります。
HTTP/1.1 429 Too Many Requests
Retry-After: 30
サーバーが指定している場合は、自分で計算した待ち時間より、Retry-Afterを優先します。値は秒数のほか、Retry-After: Wed, 30 Sep 2026 10:00:00 GMT のように日時で指定される場合もあります。
リトライする前に確認すべき「冪等性」
冪等(べきとう)とは、同じ操作を何回行っても、結果が1回行ったときと同じになる性質です。
タイムアウトしたリクエストは、サーバー側では処理が完了している可能性があります。例えば「決済する」リクエストがタイムアウトし、実は決済が完了していた場合、リトライすると二重に決済されてしまいます。
- GET・PUT・DELETE — HTTPの仕様上、冪等なメソッド。リトライしやすい
- POST — 冪等ではない。そのままリトライすると、登録や決済が重複する可能性がある
POSTをリトライする場合は、冪等キー(Idempotency-Key)をリクエストに付け、サーバー側で「同じキーのリクエストは1回しか処理しない」ようにしておくのが定石です。決済APIなどでは、この仕組みが用意されていることが多くあります。
回数と、全体の締め切りを決める
リトライには、回数の上限と、全体でどれだけ待てるかの締め切りの両方を設定します。
| 処理の種類 | 回数の目安 | 全体の締め切りの考え方 |
|---|---|---|
| ユーザーが画面で待っている処理 | 2〜3回 | 数秒以内。それ以上はエラー表示の方が親切 |
| バックグラウンドのAPI連携 | 3〜5回 | 数十秒〜数分 |
| バッチ処理・キューのジョブ | 5〜10回 | 数分〜数時間。最後は失敗キューに移す |
画面で待っているユーザーにとって、30秒のリトライは「固まった」のと同じです。誰がその結果を待っているかで、回数と締め切りを決めます。
実装例
Python
import random
import time
import urllib.request
import urllib.error
RETRYABLE_STATUS = {408, 429, 500, 502, 503, 504}
def call_with_retry(url, max_attempts=5, base=0.5, cap=20.0, deadline=30.0):
start = time.monotonic()
for attempt in range(max_attempts):
try:
with urllib.request.urlopen(url, timeout=5) as res:
return res.read()
except urllib.error.HTTPError as e:
if e.code not in RETRYABLE_STATUS:
raise # 400・401・404などはリトライしない
retry_after = e.headers.get("Retry-After")
error = e
except (urllib.error.URLError, TimeoutError) as e:
retry_after = None
error = e
if attempt == max_attempts - 1:
raise error # 最後の試行も失敗
if retry_after and retry_after.isdigit():
wait = float(retry_after) # サーバーの指定を優先
else:
wait = random.uniform(0, min(cap, base * 2 ** attempt)) # Full Jitter
if time.monotonic() - start + wait > deadline:
raise error # 全体の締め切りを超えるなら諦める
time.sleep(wait)
JavaScript(Node.js 18以降・ブラウザ)
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);
async function fetchWithRetry(url, { maxAttempts = 5, baseMs = 500, capMs = 20000 } = {}) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
let res;
try {
res = await fetch(url, { signal: AbortSignal.timeout(5000) });
if (res.ok || !RETRYABLE.has(res.status)) return res; // 成功、またはリトライしないエラー
} catch (e) {
if (attempt === maxAttempts - 1) throw e; // 通信エラー・タイムアウト
}
if (attempt === maxAttempts - 1) return res;
const retryAfter = Number(res?.headers.get("Retry-After"));
const wait = retryAfter > 0
? retryAfter * 1000 // サーバーの指定を優先
: Math.random() * Math.min(capMs, baseMs * 2 ** attempt); // Full Jitter
await sleep(wait);
}
}
どちらも、GETのような冪等なリクエストを前提にした例です。POSTに使う場合は、冪等キーを付けてください。
ライブラリ・SDKを使う
実務では、実績のあるライブラリを使うと、細かな考慮漏れを防げます。
| 言語・環境 | 主な選択肢 |
|---|---|
| Python | tenacity |
| Java | Resilience4j、Spring Retry |
| JavaScript | p-retry |
| .NET | Polly |
| AWS SDK | リトライ(指数バックオフ+ジッター)が標準で組み込まれている |
| Google Cloud クライアントライブラリ | リトライの設定が組み込まれている |
クラウドのSDKを使っている場合は、SDK自体がすでにリトライしていることが多いので、その外側で自前のリトライを重ねていないか確認します(次の「増幅」の問題につながります)。
リトライの増幅に注意する
リトライで最も見落とされやすいのが、複数の層でリトライを重ねたときの増幅です。
ブラウザ → API → 内部サービス → データベース
各層がそれぞれ「最大3回試行」すると、一番奥では最大3 × 3 × 3 = 27回のアクセスが発生します。障害でデータベースが遅くなっているときに、アクセスが27倍になれば、回復はさらに遠のきます。
- リトライは、原則として1つの層だけで行う(多くの場合、最も外側か、障害に最も近い層)
- 内側の層は、リトライせずにすぐエラーを返す
- システム全体で、リトライに使ってよいリクエストの割合を決める(リトライ予算)
サーキットブレーカーと組み合わせる
障害が長引いているときは、リトライを続けるより、しばらく呼び出しを止める方が、呼び出す側と呼ばれる側の両方にとって有益です。
サーキットブレーカーは、失敗が一定の割合を超えたら、一定時間は呼び出しをせずにすぐエラーを返し、時間が経ったら少しだけ試して回復を確認する仕組みです。
- 指数バックオフ — 1回の処理の中で、どう再試行するか
- サーキットブレーカー — システム全体として、いつ呼び出しを止めるか
前述のResilience4jやPollyなどのライブラリは、両方を組み合わせて使えます。
よくある質問
Q. 2倍ではなく、別の倍率にしてもいいですか? A. 問題ありません。2倍が一般的ですが、1.5倍などにして間隔の伸びを緩やかにすることもあります。重要なのは、上限とジッターを必ず組み合わせることです。
Q. 基準時間はどれくらいにすべきですか? A. 画面の操作に関わるAPIなら数百ミリ秒、バックグラウンドの処理なら1秒程度から始めることが多いです。相手のサービスのドキュメントに推奨値があれば、それに従います。
Q. タイムアウトの時間もリトライと関係しますか? A. 関係します。1回のタイムアウトが30秒で3回リトライすると、最大で1分半以上待つことになります。1回あたりのタイムアウトと、全体の締め切りをセットで設計します。
Q. 固定間隔のリトライを使ってよい場面はありますか? A. 呼び出し先が1つで、呼び出す側も1つしかないような、アクセスの集中が起きない場面なら、固定間隔でも問題になりにくいです。複数のクライアントから同じサービスを呼ぶ場合は、指数バックオフとジッターを使います。
まとめ
- 指数バックオフは、待ち時間を倍々に延ばすリトライ方法。上限は必ず設ける
- 同時に失敗したクライアントの一斉リトライを防ぐため、ジッター(迷ったらFull Jitter)を加える
- リトライするのはタイムアウト・408・429・502・503・504など。400・401・403・404・422はリトライしない
- Retry-Afterヘッダーがあれば、それに従う
- POSTのリトライは、冪等キーで二重処理を防ぐ
- 回数と全体の締め切りは、誰が結果を待っているかで決める
- 複数の層でリトライを重ねると増幅する。長い障害にはサーキットブレーカーを組み合わせる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「指数バックオフとは?リトライ間隔の決め方とジッターの実装」
株式会社RJCでは、「失敗したときにどう振る舞うか」まで設計することが、安心して使えるシステムにつながると考えています。こうした知識を仲間と共有しながら、一緒に成長を楽しんでいます。
株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。