Amazon Bedrock APIレート制限対策 - ThrottlingException解決のための3つのリトライ戦略比較

Amazon Bedrock APIレート制限対策 - ThrottlingException解決のための3つのリトライ戦略比較

この記事は、Insight Edge Advent Calendar 2025の4日目の記事です!!

はじめに

こんにちは。データサイエンティストの唐澤です。

業務でAmazon Bedrockを利用する機会があったのですが、複数のリクエストを並列で処理すると ThrottlingException が頻発する問題に遭遇しました。この記事では、その時の経験をもとに、どのようなリトライ戦略が効果的かをシミュレータで検証した結果を共有します。

目次

課題:ThrottlingException

Amazon BedrockのAPIを並列呼び出ししていると、以下のエラーに遭遇しました。

ThrottlingException: An error occurred (ThrottlingException) when calling the
InvokeModel operation (reached max retries: 4): Too many requests, please wait
before trying again.

原因:TPM(Tokens per minute)制限

原因を調査すると、Amazon BedrockのTPM(Tokens Per Minute)制限が関係していました。

TPMは1分間に使用できるトークン数の上限を表し、この制限を超えたために、ThrottlingExceptionが発生していました。

自分が使用していたClaude Sonnetは、デフォルトで200,000 TPMに制限されていました。

5倍のクォータを消費するモデル

さらに調査を進めると、特徴的な仕様が見つかりました。AWS公式ドキュメント「How tokens are counted in Amazon Bedrock」によると:

The burndown rate for the following models is 5x for output tokens (1 output token consumes 5 tokens from your quotas): - Anthropic Claude Opus 4 - Anthropic Claude Opus 4.1 - Anthropic Claude Sonnet 4.5 - Anthropic Claude Sonnet 4

つまり、Claude Sonnetでは1 output token がクォータからは5トークン差し引かれます

Note: You're only billed for your actual token usage.

課金は実際のトークン使用量に対して発生しますが、レート制限の計算では5倍のクォータを消費するため、これらのモデルは特にレート制限に引っかかりやすくなっています。

解決策:リトライ

実務では、試行錯誤しながらmax_tokensの調整やリトライ処理を実装し、エラーの発生を抑えられました。

この経験をもとに、TPM制限に対して具体的にどのようなリトライ戦略が効果的なのかを、シミュレーションで検証したいと思います。

今回は、代表的な3つのリトライ戦略を比較検証します。

比較する戦略

1. Constant Backoff

常に一定時間待機する戦略です。

def constant_backoff(retry_count: int, base_delay: int = 5) -> int:
    """常に一定時間待機"""
    return base_delay

2. Linear Backoff

待ち時間を線形に増やす戦略です。

def linear_backoff(retry_count: int, base_delay: int = 5) -> int:
    """retry_count 0: 5秒, 1: 10秒, 2: 15秒, 3: 20秒..."""
    return base_delay * (retry_count + 1)

3. Exponential Backoff

待ち時間を指数的に増やす戦略です。

def exponential_backoff(retry_count: int, base_delay: int = 5) -> int:
    """retry_count 0: 5秒, 1: 10秒, 2: 20秒, 3: 40秒..."""
    return base_delay * (2 ** retry_count)

※ 本実装は簡易的なものです。キャップをかけたりJitterを加えるといった工夫もあります。より詳しい実装については、AWSの公式ブログでも解説されています。

シミュレータによるリトライ戦略の定量的比較

シミュレーション条件

  • リクエスト数: 20件(すべて同時刻に到着したと想定)
  • 各リクエストのトークン数: 20,000〜40,000(ランダム)
  • レート制限: 200,000 tokens/min
  • 最大リトライ回数: 5回
  • 処理時間: トークン数に比例(50,000トークン当たりの処理に1分掛かるものとする)

※ 本シミュレーションでは、簡略化のため、各リクエストのトークン数がそのままクォータから差し引かれるものとします。

シミュレータ実装(抜粋)

リクエストの状態を管理するデータクラスです。

@dataclass
class Request:
    """リクエストの状態を管理"""
    id: int
    tokens: int
    next_try_time: int = 0  # 次に処理を試みる時刻
    complete_time: int = 0  # 処理完了時刻
    retry_count: int = 0
    status: str = "pending"  # pending/processing/retry_waiting/success/failed

トークンの管理とレート制限を実装したクラスです。

class RateLimiter:
    def __init__(self, max_tokens_per_minute: int = 200000):
        self.max_tokens_per_minute = max_tokens_per_minute
        self.available_tokens = max_tokens_per_minute
        self.current_time = 0
        self.last_recovery_time = 0

    def can_process(self, tokens: int) -> bool:
        """リクエストが処理可能かチェック"""
        return tokens <= self.available_tokens

    def consume_tokens(self, tokens: int):
        """トークンを消費"""
        self.available_tokens -= tokens

    def advance_time(self, seconds: int = 1):
        """時間を進めてトークンを回復"""
        self.current_time += seconds

        # 1分ごとにトークンを回復
        time_since_recovery = self.current_time - self.last_recovery_time
        if time_since_recovery >= 60:
            minutes = int(time_since_recovery // 60)
            self.available_tokens = self.max_tokens_per_minute
            self.last_recovery_time += minutes * 60

並列リクエストのシミュレータでは、1秒ずつ時間を進めながら、各時刻でリクエストの処理とリトライを行います。

# 1秒ずつ時間を進めるシミュレーション
while True:
    # 処理中または待機中のリクエストがあるかチェック
    active_requests = [r for r in requests
                      if r.status in ["pending", "processing", "retry_waiting"]]
    if not active_requests:
        break

    # 時間を1秒進める
    rate_limiter.advance_time(1)

    # この時刻に処理を試みるリクエストを取得
    requests_to_try = [r for r in requests
                      if r.status in ["pending", "retry_waiting"]
                      and r.next_try_time <= rate_limiter.current_time]

    # 各リクエストを処理
    for req in requests_to_try:
        if rate_limiter.can_process(req.tokens):
            # 成功 - 処理開始
            rate_limiter.consume_tokens(req.tokens)
            req.status = "processing"
            processing_time = int(req.tokens * 0.0012)  # 50,000トークンで60秒
            req.complete_time = rate_limiter.current_time + processing_time
        else:
            # 失敗 - リトライをスケジュール
            if req.retry_count < max_retries:
                wait_time = retry_strategy(req.retry_count)
                req.next_try_time = rate_limiter.current_time + wait_time
                req.retry_count += 1

検証結果

# 戦略 総時間 成功 失敗 成功率 リトライ回数
1 Constant Backoff (60秒) 168秒 20 0 100% 17回
2 Exponential Backoff 203秒 20 0 100% 53回
3 Linear Backoff 123秒 15 5 75% 60回
4 Constant Backoff (5秒) 35秒 8 12 40% 60回

結果の考察

Constant Backoff (60秒)は総処理時間が短く(168秒vs 203秒)、リトライ回数も少ない(17回vs 53回)結果となりました。

なぜ100%の成功率となったのでしょうか?

原因を探るために、今回のシミュレーションにおける各リクエストのトークン数を確認してみましょう。

リクエストID トークン数 リクエストID トークン数
0 23,648 10 33,825
1 20,819 11 21,041
2 29,012 12 20,976
3 28,024 13 23,070
4 27,314 14 27,164
5 24,572 15 27,623
6 23,358 16 36,559
7 37,870 17 39,726
8 22,848 18 20,869
9 39,349 19 38,390

合計: 566,057トークン

20個のリクエストが同時に行われた場合を考えます。レート制限の観点では、これらを全て処理するには約2.8分(566,057 ÷ 200,000 ≈ 2.8)必要です。つまり、1回のトークン回復(60秒)だけでは処理しきれず、最低でも2回の回復が必要な負荷状況となっています。

成功した戦略の分析

Constant Backoff (60秒) は、トークン回復の周期(60秒)に待ち時間を合わせることで、効率的に2回分のトークン回復タイミング(60秒、120秒)を待つことができました。今回のシミュレーションでは最も速く(168秒)、かつリトライ回数も最小(17回)で全リクエストの処理に成功しています。

Exponential Backoff の待ち時間は指数的に増加します(5秒 → 10秒 → 20秒 → 40秒 → 80秒)。この特性により、2回分のトークン回復タイミングを待てました。シミュレーションでは1分おきにトークンの回復処理を行いましたが、トークン回復のタイミングや周期を事前に知らなくても、徐々に待ち時間を増やすことが可能です。

上図は、トークンの消費と回復の様子を示しています。時刻1秒で8件、時刻76秒(1回目の回復後)で7件、時刻156秒(2回目の回復後)で残り5件が処理され、最終的に全20件が成功しました(総処理時間203秒)。

なお、一定時間内のリクエスト数に上限がある場合(例:RPS - Requests Per Second)、リトライタイミングを分散させることが有効です。筆者は今回のTPM制限とは別のケースで、1秒当たりのリクエスト回数制限に引っかかった際、Exponential BackoffにJitterを加えることでリトライタイミングを分散させ、問題を回避できた経験があります。

一方、Constant Backoff (5秒)とLinear Backoffでは、トークンが回復しないうちに最大リトライ回数に達してしまい、成功率100%とはなりませんでした。

今回の結果から、レート制限の仕組み(トークン回復の仕組み)を理解している場合はConstant Backoffで適切な待ち時間を設定するのが効率的であることが見えてきました。ただし、待ち時間を長く設定すると、低負荷時には無駄な待ち時間が発生する恐れがあることには注意が必要です。例えば、トークン回復の直前(回復の1秒前など)にリクエストが到着しTPM制限を超えた場合でも、Constant Backoff (60秒)では次の回復まで60秒待つことになります。

まとめ

リトライ戦略によって総処理時間や成功するリクエスト数に違いがあることが確認できました。

この検証はあくまで簡易的なシミュレーションです。実際のシステムに適用する際は、以下をはじめとする項目を見積もったうえで、適切なリトライ戦略を検討する必要があると考えています:

  • ピーク時のリクエスト数
  • 1リクエストあたりの平均トークン数(input + output)
  • 許容できる処理時間

利用規模が大きい場合は、クォータ上限の引き上げも検討できると良いでしょう。 また、過度なリトライによるサーバー側への負荷も考慮する必要があります。

実務では試行錯誤の末に問題を解決しました。今回の記事では、その経験をもとに、シミュレーションを通じてリトライ戦略の検討ポイントを整理しました。課題に直面した際、諦めずに考え抜き、実践する――Insight Edgeの「やりぬく」というValueを、改めて意識する機会となりました。

参考