クラウドのAPIを呼び出していると、突然「ThrottlingException」や「ServiceUnavailable(503)」が返ってきて困ったことはないでしょうか。オンプレのシステムでは「もう一度叩けば通る」で済ませていた感覚が、クラウドでは通用しないことがあります。再試行のやり方次第で、システムの安定性だけでなくコストまで変わってくるからです。
この記事では、クラウド設計の基本パターン「リトライパターン」と「指数バックオフ(Exponential Backoff)」を、オンプレ経験者にもわかりやすく解説します。なぜ単純なループリトライが危険なのか、指数バックオフとジッターがなぜ必要なのか、AWS SDKやStep Functionsでどう設定するのかまで、現場で使えるレベルで掘り下げます。
なぜクラウドではリトライ設計が必要なのか
オンプレ環境では、サーバーは物理的に近く、ネットワーク帯域も占有しているため、「APIエラー=バグか設定ミス」という二択がほとんどでした。クラウドは事情が違います。
・スロットリング(Throttling): AWSのAPIは秒間リクエスト数に上限があり、超えると「ThrottlingException(HTTP 429)」が返ります。EC2 Auto Scaling APIやDynamoDBのプロビジョニング上限超えがその代表例です。
・一時的なサービス障害(Transient Error): クラウドプロバイダー側の一時的な問題で5xxエラーが返ることがあります。SLAが99.99%のサービスでも、月換算で約52分のダウンタイムが許容されています。
・ネットワーク輻輳: マルチテナント環境では、他テナントのトラフィック増加によるパケットロスや遅延が予測できません。
・Cold Start後の遅延: Lambdaなどサーバーレス環境では、コールドスタート直後にタイムアウトに近い応答時間になることがあります。
これらは「一時的なエラー(Transient Error)」と呼ばれます。少し待って再試行すれば解決するエラーです。これがリトライパターンの出発点です。
オンプレ経験者にとって重要な認識の転換があります。オンプレでは「エラーは異常」でしたが、クラウドでは「一時的なエラーは正常の範囲内」として設計に織り込む必要があります。これを「フォールトトレラントな設計」と呼びます。
リトライパターンの3段階進化
1. 単純なリトライ(最初の一歩・でも危険)
まず思いつくのが「エラーが出たらすぐ再試行する」シンプルなループです。
# シンプルリトライの例(Python) import boto3 def simple_retry(func, max_retries=3): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise # 最後のリトライでも失敗したら例外を上げる # 待機なしで即座に再試行 ← これが問題
このコードの問題点は「待機なしで即座に再試行する」点にあります。スロットリングが発生しているということは「今、APIに負荷がかかっている」状態です。即座に再試行するということは、その負荷をさらに上乗せすることになります。
100台のサーバーが同時にこのコードを実行していたら、エラーが出た瞬間に100台が一斉にリトライします。これが「リトライストーム」と呼ばれる問題で、障害を悪化させる典型的なアンチパターンです。
2. 固定間隔リトライ(少しマシだが不十分)
待機時間を入れれば改善されます。
# 固定間隔リトライ(2秒待ち) import time def fixed_interval_retry(func, max_retries=3, wait_seconds=2): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise time.sleep(wait_seconds) # 毎回2秒待つ
固定間隔リトライは「即座リトライ」より安全ですが、まだ問題があります。100台のサーバーが同時に同じ2秒待ってから再試行すると、やはり「2秒後に100台が一斉にリトライ」というパターンになってしまいます。
3. 指数バックオフ(クラウドの標準解)
「指数バックオフ(Exponential Backoff)」は、リトライするたびに待機時間を指数関数的に増やすアプローチです。
待機時間の計算式: wait = base × 2attempt
・1回目のリトライ: 1秒待ち(1 × 20)
・2回目のリトライ: 2秒待ち(1 × 21)
・3回目のリトライ: 4秒待ち(1 × 22)
・4回目のリトライ: 8秒待ち(1 × 23)
・5回目のリトライ: 16秒待ち(1 × 24)
# 指数バックオフリトライ import time def exponential_backoff_retry(func, max_retries=5, base_delay=1, max_delay=60): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise # 指数バックオフで待機(上限を設けてmax_delay秒を超えないようにする) wait_time = min(base_delay * (2 ** attempt), max_delay) time.sleep(wait_time)
待機時間に上限(`max_delay`)を設けることが重要です。指数関数は急激に増えるため、上限なしだと5回目以降は数十秒~数分待ちになることがあります。クラウドの実務では上限を30秒~60秒に設定するケースが多いです。
ジッターを加えてサンダリングハード問題を防ぐ
指数バックオフだけではまだ問題が残ります。「同じタイミングでエラーに遭遇した複数のクライアントが、同じ待機時間の後に一斉に再試行する」現象です。
たとえば100台のサーバーが同時に503エラーを受け取ると、全台が「2秒後に再試行」→全台が「4秒後に再試行」という同期されたパターンになります。これを「サンダリングハード(Thundering Herd)問題」または「リトライストーム」と呼びます。
これを防ぐのが「ジッター(Jitter)」です。待機時間にランダム性を加えて、各クライアントのリトライタイミングをバラけさせます。
# Full Jitter(推奨パターン) import time import random def retry_with_full_jitter(func, max_retries=5, base_delay=1, max_delay=60): for attempt in range(max_retries): try: return func() except Exception as e: if attempt == max_retries - 1: raise # Full Jitter: 0 ~ cap の範囲でランダムに待機 cap = min(base_delay * (2 ** attempt), max_delay) wait_time = random.uniform(0, cap) time.sleep(wait_time)
ジッターには複数の実装方式があります。
| 方式 | 計算式 | 特徴 |
|---|---|---|
| Full Jitter | random(0, min(cap, base × 2n)) | 最も分散が広い。AWSが推奨する方式 |
| Equal Jitter | cap/2 + random(0, cap/2) | 最低待機時間を保証。最大レイテンシを抑えたい場合 |
| Decorrelated Jitter | random(base, prev_wait × 3) | 前回の待機時間に基づく。スループットが高い |
AWSの公式ブログでは「Full Jitter」を推奨しています。最もリトライストームを抑制できるためです。
AWS SDKのデフォルトリトライ動作を理解する
AWS SDKは指数バックオフとジッターを標準で実装しています。ただし、デフォルト設定を把握せずに使うと「SDKが内部でリトライしているのに、外側でもリトライしていた」という二重リトライが起きることがあります。
1. Boto3(Python SDK)のリトライ設定
Boto3にはリトライモードが3種類あります(2026年時点)。
| モード | デフォルトリトライ回数 | 特徴 |
|---|---|---|
| legacy | 4回(合計5回の試行) | 旧来の動作。非推奨 |
| standard | 2回(合計3回の試行) | 推奨。指数バックオフ+Full Jitter |
| adaptive | 可変 | 実際のエラー率に応じてリクエストレートを動的調整 |
# Boto3のリトライ設定例(AWS CLI/Python) import boto3 from botocore.config import Config # standardモードでリトライ上限を5回に設定 config = Config( retries={ 'max_attempts': 5, # リトライ上限(初回含む試行回数) 'mode': 'standard' # 推奨モード } ) # DynamoDBクライアントに設定を適用 dynamodb = boto3.client('dynamodb', config=config)
# AWS CLI の場合は環境変数で設定可能 export AWS_RETRY_MODE=standard export AWS_MAX_ATTEMPTS=5
2. AWS Lambdaでのリトライ挙動に注意
Lambdaは呼び出し方式によってリトライ挙動が大きく変わります。
・同期呼び出し(Invoke API経由): SDK側がリトライを担当。Lambda自体は自動リトライしません。
・非同期呼び出し(イベントソース:S3、EventBridgeなど): Lambdaが最大2回自動リトライします(デフォルト)。最大試行回数と最大イベント経過時間の設定で変更可能です。
・ストリームベース(Kinesis、DynamoDB Streams): エラーが解消されるまでリトライを継続します。適切な「ビスポーズトキューへの送信(BisectBatchOnFunctionError)」設定が必要です。
非同期呼び出しのリトライ回数を変更するには、Lambdaの「非同期呼び出し設定」から「最大試行回数」を0(リトライなし)や1に変更できます。
リトライがコストに与える影響
リトライは「無料で何度でも試せる」わけではありません。APIコールごとに課金されるサービスでは、リトライ自体がコストになります。
・Amazon DynamoDB: 読み取り・書き込みのキャパシティユニットを消費します。スロットリングが多発してリトライが増えると、キャパシティの無駄遣いになります。
・Amazon API Gateway: APIリクエスト数が課金対象です(2026年時点: 約$3.50/100万リクエスト)。リトライ1回ごとにカウントされます。
・データ転送料金: リトライによるデータ転送が増えると、AWSのデータ転送料金(アウトバウンド)が増加します。
・AWS Lambda: 呼び出し回数とGBs(メモリ×実行時間)で課金されます。非同期リトライが増えると直接コストに跳ね返ります。
リトライを「最大5回」に設定したからといって、毎回5回リトライするわけではありませんが、障害発生時に急激にコストが増える可能性はあります。AWSのCost Anomaly Detectionを使って異常なリトライによるコスト急増を早期検知する運用がおすすめです。
応用・実務Tips
リトライすべきエラー・すべきでないエラー
リトライパターンで最もよくある失敗が「リトライしてはいけないエラーをリトライしてしまう」ことです。
| エラー種別 | HTTPステータス | リトライ判定 | 理由 |
|---|---|---|---|
| 内部サーバーエラー | 500 | リトライ可 | 一時的な障害の可能性が高い |
| サービス利用不可 | 503 | リトライ可 | 一時的な過負荷・メンテナンスの可能性 |
| スロットリング | 429 | リトライ可(必ずバックオフ) | 待機後に回復するケースが多い |
| ゲートウェイタイムアウト | 504 | リトライ可(冪等性確認必須) | リクエストが処理済みの可能性もある |
| 不正なリクエスト | 400 | リトライ禁止 | リクエスト自体が間違っており、何度試しても同じ結果 |
| 認証エラー | 401 / 403 | リトライ禁止 | 権限がない。リトライしても意味なし、ログ汚染になる |
| リソースが見つからない | 404 | リトライ禁止 | 対象リソースが存在しない |
| バリデーションエラー | 422 | リトライ禁止 | 入力データの問題。リトライで解決しない |
4xxエラー(クライアントエラー)は「リクエスト自体に問題がある」ため、何度リトライしても結果は変わりません。リトライすると無駄なAPIコールが増えるだけでなく、ログが汚染されてアラートの見落としにつながります。
冪等性との組み合わせが必須
リトライパターンを安全に使うには「冪等性(Idempotency)」との組み合わせが不可欠です。
504(ゲートウェイタイムアウト)が返った場合を考えてみてください。「タイムアウトになった」ということは、リクエストがサーバー側で処理済みかどうか判断できません。そのままリトライすると「二重実行」が起きる可能性があります。
・注文APIを2回送ったら2回注文が確定してしまった
・DynamoDBへの書き込みが2回実行された
・メール送信が2通飛んだ
冪等性を確保することで、同じリクエストを複数回送っても1回しか実行されない設計にできます。DynamoDBの条件付き書き込みや、リクエストにユニークなIDを付与するパターンがよく使われます。
冪等性の設計については、冪等性(Idempotency)設計入門で詳しく解説しています。リトライパターンと合わせて理解しておくべき重要概念です。
サーキットブレーカーパターンとの組み合わせ
リトライパターンには「連続してエラーが返っているのにリトライし続けてしまう」という弱点があります。たとえばDependencyサービス全体が5分間ダウンしている場合、リトライを繰り返してもシステム全体に無駄な負荷がかかるだけです。
このケースでは「サーキットブレーカーパターン」と組み合わせることで解決できます。エラーが一定数を超えたら「回路を開いて(Open)」、リクエストを即失敗させてリトライを一時的に停止します。詳しくはサーキットブレーカーパターン入門をご覧ください。
AWS Step Functionsのリトライ設定
マイクロサービスのワークフロー管理にAWS Step Functionsを使っている場合、ステートマシンの定義にリトライポリシーを直接記述できます。コード側でリトライ処理を実装する必要がなくなるため、ロジックがシンプルになります。
# Step Functions ステートマシン定義(ASL: Amazon States Language) { "Comment": "指数バックオフリトライ付きの注文処理ワークフロー", "StartAt": "ProcessOrder", "States": { "ProcessOrder": { "Type": "Task", "Resource": "arn:aws:lambda:ap-northeast-1:ACCOUNT:function:process-order", "Retry": [ { "ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"], "IntervalSeconds": 2, # 初回待機秒数 "MaxAttempts": 5, # 最大リトライ回数 "BackoffRate": 2.0, # バックオフ乗数(指数バックオフ) "MaxDelaySeconds": 60 # 最大待機秒数(上限設定) } ], "Catch": [ { "ErrorEquals": ["States.ALL"], "Next": "HandleError" } ], "End": true } } }
`BackoffRate: 2.0` が指数バックオフの乗数です。`IntervalSeconds: 2` を基点に、リトライのたびに2倍(2秒→4秒→8秒→16秒→32秒)になります。`MaxDelaySeconds` で上限も設定できるため、ジッターを別途実装せずとも分散が確保されます。
Step Functionsの詳しい使い方はAWS Step Functions入門でも解説しています。
よくあるトラブルと対処法
Q. Boto3がリトライしているのにまだエラーが出る
A. Boto3のデフォルトリトライ回数(standardモードで3回)を超えてもエラーが解消しない場合、スロットリングが慢性化している可能性があります。DynamoDBであればキャパシティ設定を見直す、あるいはDAXを導入してDB負荷を軽減することを検討してください。
Q. リトライ中にLambdaの実行時間が超過してしまう
A. Lambda関数のタイムアウトはデフォルト3秒です。指数バックオフで待機時間が積み重なるとタイムアウトに達することがあります。Lambdaのタイムアウト値を適切に設定するか、リトライロジックをStep Functionsに外出しにして、タイムアウト問題を回避する設計を検討してください。
Q. テスト環境でリトライを再現する方法がわからない
A. Boto3の`botocore.stub`モジュールを使うと、特定のAPIコールに対してエラーレスポンスを返すスタブを作成できます。また、LocalStackを使ってAWSサービスをローカル環境でエミュレートしながらスロットリングシナリオをテストする方法も有効です。
Q. 非同期Lambdaのリトライがイベントを重複処理してしまう
A. 非同期呼び出しのリトライによるイベント重複はよくある問題です。DLQ(Dead Letter Queue)とSQSを組み合わせて、リトライ上限を超えたイベントを安全に隔離する仕組みを用意することが重要です。Bulkheadパターンでの障害隔離設計も参考になります。
Q. 「ジッターを入れると最大レイテンシが予測できなくなる」と言われた
A. Full Jitter(0~capのランダム)では確かに待機時間の下限が0になります。「最低でも一定時間待つ」保証が必要な場合はEqual Jitter(cap/2 + random(0, cap/2))を選ぶと、最大レイテンシを抑えながらジッター効果も得られます。
本記事のまとめ
| 概念 | ポイント |
|---|---|
| リトライパターン | 一時的エラー(5xx・429)は再試行で回復できる。4xx(クライアントエラー)はリトライ禁止 |
| 指数バックオフ | base × 2n で待機時間を増やす。上限(maxDelay)を設定して無限増加を防ぐ |
| ジッター | 待機時間にランダム性を加えてリトライストームを防ぐ。Full Jitterが推奨 |
| AWS SDK設定 | Boto3はstandardモードで指数バックオフ+ジッターを標準搭載。max_attemptsで上限設定 |
| Lambdaの注意点 | 呼び出し方式によりリトライ挙動が異なる。タイムアウト超過に注意 |
| Step Functions | BackoffRate・MaxDelaySecondsでリトライを宣言的に定義。コード不要 |
| 冪等性との組み合わせ | タイムアウト後のリトライで二重実行を防ぐために必須の概念 |
| サーキットブレーカーとの組み合わせ | 連続エラー時はリトライを一時停止して障害の連鎖を防ぐ |
リトライパターンは「エラーハンドリング」ではなく「設計パターン」として捉えることが大切です。単純なループリトライから指数バックオフ+ジッターへの進化は、クラウドの特性を理解した上での必然的な選択です。AWS SDKのリトライ設定を正しく理解し、冪等性・サーキットブレーカーと組み合わせることで、クラウドAPIの一時的エラーに強い、本番品質のシステムを構築できます。
PR
リトライパターン・サーキットブレーカー・Sagaパターンなど、クラウドアーキテクチャの設計パターンを体系的に網羅した一冊。AWSサービスとの具体的な組み合わせ方まで解説しており、設計力を高めたいインフラエンジニアに特におすすめです。
