はじめに:指数バックオフは「だんだん距離を取りながら、もう一度だけ試す」リトライ戦略
外部 API、クラウドサービス、DB、メッセージキューなどにアクセスするとき、
一時的なエラー(ネットワークの瞬断、サーバーの一時負荷など)は「少し時間をおいて再試行すれば成功する」ことがよくあります。
ただし、失敗するたびに毎回同じ間隔で連打すると、相手のサービスにさらに負荷をかけ、自分のアプリ側も無駄にリソースを消費し、ログは「失敗→即リトライ→即失敗…」で埋まります。
そこで登場するのが 指数バックオフ(Exponential Backoff) です。
ざっくり言うと、「失敗するたびに、次のリトライまでの待ち時間を“倍々”に伸ばしていく」戦略です。
ここでは、初心者向けに
指数バックオフの考え方
C# でのシンプルな実装
どの例外をリトライ対象にするか
ログとの組み合わせ方
実務での使いどころと注意点
を、例題付きで丁寧に解説します。
指数バックオフの基本イメージ
待ち時間が「一定」ではなく「倍々に増えていく」
まずは数字でイメージをつかみましょう。
初回の待ち時間を 500ms にした場合、例えば次のようになります。
1 回目の失敗後の待ち時間:0.5 秒
2 回目の失敗後の待ち時間:1.0 秒
3 回目の失敗後の待ち時間:2.0 秒
4 回目の失敗後の待ち時間:4.0 秒
このように、待ち時間が 2 倍ずつ増えていきます。
もちろん、倍率は 2 倍に限らず、1.5 倍や 3 倍などにすることもあります。
ここでの重要ポイントは、「失敗が続くほど“間を空けて様子を見る”」という姿勢です。
すぐに連打するのではなく、「相手が回復する時間」を意識して距離を取るイメージです。
C# でのシンプルな非同期指数バックオフ実装
基本形:ExecuteWithExponentialBackoffAsync
まずは、async/await に対応したシンプルな指数バックオフ付きリトライユーティリティを書いてみます。
public static class RetryPolicy
{
public static async Task ExecuteWithExponentialBackoffAsync(
Func<Task> action,
int maxRetryCount,
TimeSpan initialDelay,
Func<Exception, bool> shouldRetry)
{
var delay = initialDelay;
for (int attempt = 1; attempt <= maxRetryCount; attempt++)
{
try
{
await action();
return; // 成功したら終了
}
catch (Exception ex)
{
// リトライ対象外、または最後の試行なら、そのまま投げる
if (!shouldRetry(ex) || attempt == maxRetryCount)
{
throw;
}
// 次のリトライまで待つ
await Task.Delay(delay);
// 待ち時間を倍にする(指数バックオフ)
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
}
}
}
}
C#使い方の例です。
await RetryPolicy.ExecuteWithExponentialBackoffAsync(
async () => await CallExternalApiAsync(),
maxRetryCount: 5,
initialDelay: TimeSpan.FromMilliseconds(500),
shouldRetry: ex => ex is HttpRequestException);
C#ここでの重要ポイントは三つあります。
一つ目は、「どの処理をリトライするか」を Func<Task> で渡していることです。これにより、任意の非同期処理を包めます。
二つ目は、「最大リトライ回数」と「初期待ち時間」を引数で指定できることです。これにより、処理ごとにポリシーを変えられます。
三つ目は、「shouldRetry で“どの例外だけリトライ対象にするか”を制御している」ことです。ここが安全性の要です。
どの例外をリトライ対象にするか
「一時的な失敗」だけを狙い撃ちする
指数バックオフは便利ですが、何でもかんでもリトライしてはいけません。
リトライすべきなのは、主に「一時的な環境要因による失敗」です。
例えば、次のようなものです。
ネットワークの一時障害(HttpRequestException)
外部サービスの一時的な 5xx エラー
DB の一時的なロックやタイムアウト(特定の例外)
逆に、次のようなものはリトライしても意味がありません。
ユーザー入力エラー(ArgumentException など)
ビジネスルール違反(「在庫不足」「締め切り超過」など)
プログラムバグ(NullReferenceException など)
これらは「何回やり直しても成功しない」タイプのエラーです。
そのため、shouldRetry には「一時的な失敗だけを判定するロジック」を書きます。
bool ShouldRetry(Exception ex)
{
if (ex is HttpRequestException)
{
return true;
}
// 必要なら、特定のステータスコードだけ許可するなどの判定もここに書く
return false;
}
C#ここでの重要ポイントは、「リトライ対象の例外を明示的に絞る」ことです。
“直してもらわないといけないエラー”はリトライせず、すぐに失敗として扱うべきです。
ログと組み合わせた指数バックオフユーティリティ
「何回目で成功したか」「どこで諦めたか」を残す
実務では、指数バックオフの挙動をログに残しておくと、
「この API はよく 3 回目でやっと成功しているな」といった分析ができるようになります。
ILogger と組み合わせた例を見てみます。
public static class RetryPolicyWithLogging
{
public static async Task ExecuteWithExponentialBackoffAsync(
Func<Task> action,
int maxRetryCount,
TimeSpan initialDelay,
Func<Exception, bool> shouldRetry,
ILogger logger,
string context)
{
var delay = initialDelay;
for (int attempt = 1; attempt <= maxRetryCount; attempt++)
{
try
{
await action();
if (attempt > 1)
{
logger.LogInformation(
"指数バックオフリトライ成功。Context={Context} Attempts={Attempts}",
context,
attempt);
}
return;
}
catch (Exception ex)
{
if (!shouldRetry(ex) || attempt == maxRetryCount)
{
logger.LogError(
ex,
"指数バックオフリトライ失敗(打ち切り)。Context={Context} Attempts={Attempts}",
context,
attempt);
throw;
}
logger.LogWarning(
ex,
"指数バックオフリトライ対象の例外。Context={Context} Attempt={Attempt} 次は {Delay} 後に再試行します。",
context,
attempt,
delay);
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
}
}
}
}
C#使い方の例です。
await RetryPolicyWithLogging.ExecuteWithExponentialBackoffAsync(
async () => await CallExternalApiAsync(),
maxRetryCount: 5,
initialDelay: TimeSpan.FromMilliseconds(500),
shouldRetry: ex => ex is HttpRequestException,
logger: _logger,
context: "顧客情報連携API");
C#ここでの重要ポイントは、「リトライの回数と文脈(どの処理か)をログに残す」ことです。
これにより、「リトライ頼みになっていないか」「どの処理が不安定か」を後から可視化できます。
実務での使いどころと注意点
どこに組み込むか:外部依存の“近く”に置く
指数バックオフ付きリトライは、主に「外部依存の近く」に置くのがきれいです。
外部 API クライアントクラス(HTTP クライアント)
DB アクセスを包むリポジトリ層
メッセージキューやストレージへのアクセスクラス
こういった場所で、「このメソッドは内部で指数バックオフ付きリトライを行う」と決めておくと、
上位のサービス層や UI 層は「ただ呼ぶだけ」で済みます。
逆に、サービス層や UI 層のあちこちに「for+try-catch+Delay」が散らばると、
どこで何回リトライしているのか分からなくなり、調整も困難になります。
ここでの重要ポイントは、「リトライポリシーを“インフラ寄りの層”に閉じ込める」ことです。
そうすることで、ポリシー変更(回数や待ち時間の調整)も一箇所で済みます。
やりすぎ注意:リトライは“魔法”ではない
指数バックオフは強力ですが、万能ではありません。
次のような点には注意が必要です。
最大リトライ回数を大きくしすぎると、ユーザーから見たレスポンスが極端に遅くなる
外部サービス側が完全に落ちている場合、何回待っても無駄
「リトライすれば何とかなる」と思って、根本原因の調査をサボりがちになる
大事なのは、「リトライは一時的な揺らぎへの対処であって、恒久的な解決ではない」という意識です。
ログを見て、「いつも 4 回目でやっと成功している」ような処理があれば、
根本的な改善(キャッシュ、設計見直し、サービス側の調整など)を検討すべきです。
まとめ:指数バックオフは“賢く距離を取りながら再挑戦する”ためのユーティリティ
指数バックオフの本質を一言で言うと、
「一時的な失敗に対して、
すぐに連打するのではなく、
待ち時間を伸ばしながら、決めた回数だけ丁寧に再挑戦する」
というリトライの作法です。
押さえておきたいポイントを整理すると、次のようになります。
待ち時間を「一定」ではなく「倍々に増やす」ことで、相手にも自分にも優しいリトライになる。
リトライ対象は「一時的な失敗」に絞り、ビジネスエラーやバグはリトライしない。Func<Task>、最大回数、初期待ち時間、shouldRetry をパラメータにしたユーティリティにすると再利用しやすい。
ILogger と組み合わせて、「何回目で成功したか」「どこで諦めたか」をログに残すと、後から健康状態を分析できる。
外部依存の近く(API クライアント、リポジトリなど)に組み込み、上位層からは意識せず使えるようにする。
ここまでイメージできていれば、「とりあえず同じ処理を連打する」段階から卒業して、
“設計されたリトライ戦略”として指数バックオフをユーティリティに組み込めるようになります。
