AWS Bedrock throttling GenAI quota

AWS Bedrock - ThrottlingException: too many tokens per day, please wait before trying again

Naprawa ThrottlingException 'too many tokens per day' w AWS Bedrock: analiza limitów dziennych, retry z exponential backoff, zwiększenie limitów, fallback na alternatywne modele.

Jerzy Kopaczewski ·
Aplikacja korzystająca z AWS Bedrock (InvokeModel / Converse API) zaczyna zwracać HTTP 429 z komunikatem: ThrottlingException: Too many tokens per day, please wait before trying again. Problem pojawia się po osiągnięciu dziennego limitu tokenów dla danego modelu (np. Claude, Llama). Wszystkie kolejne żądania są odrzucane do resetu limitu o północy UTC. Aplikacja staje się niefunkcjonalna na resztę dnia.

Ten runbook opisuje diagnozę i obejście dziennego limitu tokenów w AWS Bedrock. Architekturę wnioskowania na AWS i porównanie kosztów opisujemy w artykule LLM Inference na AWS - Bedrock vs SageMaker vs Self-Hosted na EKS. Budowę pipeline’u RAG z Knowledge Base omawiamy w Architektura RAG na AWS. Jeśli potrzebujesz pomocy z optymalizacją kosztów i limitów Bedrock - umów konsultacje.

Objaw

Bedrock API zwraca błąd 429 z komunikatem wskazującym na dzienny limit tokenów:

# Typowy komunikat błędu w logach aplikacji:
# botocore.exceptions.ClientError: An error occurred (ThrottlingException) when calling
# the InvokeModel operation: Too many tokens per day, please wait before trying again.

# Warianty komunikatu:
# "Too many tokens per day, please wait before trying again."
# "You have exceeded your token quota for the day."
# "Rate exceeded: tokens per day limit reached for model anthropic.claude-3-5-sonnet-20241022-v2:0"

# Sprawdź CloudWatch metryki Bedrock - zużycie tokenów
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name InputTokenCount \
  --dimensions Name=ModelId,Value=anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Sum \
  --query 'Datapoints[*].{time:Timestamp,tokens:Sum}' | jq 'sort_by(.time)'

# Sprawdź OutputTokenCount analogicznie
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name OutputTokenCount \
  --dimensions Name=ModelId,Value=anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 3600 \
  --statistics Sum \
  --query 'Datapoints[*].{time:Timestamp,tokens:Sum}' | jq 'sort_by(.time)'

# Sprawdź metrykę Invocation4xxErrors (liczy throttled requests)
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name Invocation4xxErrors \
  --dimensions Name=ModelId,Value=anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 300 \
  --statistics Sum

# Sprawdź aktualny service quota dla modelu
aws service-quotas get-service-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --query '{quotaName:QuotaName, value:Value, unit:Unit}'

Kluczowe rozróżnienie - Bedrock ma trzy rodzaje limitów:

Typ limitu Komunikat Reset Rozwiązanie
Tokens per minute (TPM) “Too many requests” / rate limit Po 60s Retry z backoff
Requests per minute (RPM) “Too many requests” Po 60s Retry z backoff
Tokens per day (TPD) “Too many tokens per day” Północ UTC Ten runbook

Przyczyna

AWS Bedrock nakłada trzy warstwy limitów na każdy model, per region, per konto:

1. Domyślny limit dzienny jest niski dla nowych kont:

Nowo aktywowane modele mają niskie domyślne limity dzienne. Np. Claude 3.5 Sonnet na nowym koncie może mieć limit 1M tokenów/dzień. Przy średnim zapytaniu RAG (~4K input + 1K output = 5K tokenów) to zaledwie 200 zapytań dziennie.

2. Input tokens + output tokens sumują się do limitu:

Limit „tokens per day” to suma input tokens (prompt) + output tokens (completion). Długie system prompts (typowe w RAG: 2-8K tokenów) zjadają limit szybciej niż się wydaje.

3. Brak widocznego ostrzeżenia przed osiągnięciem limitu:

Bedrock nie zwraca nagłówka z informacją o pozostałym limicie (w przeciwieństwie do OpenAI API). Jedynym sposobem monitorowania jest CloudWatch metrics. Bez alertu aplikacja nagle przestaje działać.

4. Reset o północy UTC - nie o lokalnej północy:

Limit resetuje się o 00:00 UTC. Dla zespołu w CET (UTC+1/+2) oznacza to reset o 01:00-02:00 w nocy. Aplikacja osiągająca limit o 16:00 czasu lokalnego jest niefunkcjonalna przez 9-10 godzin.

5. Provisioned Throughput (PT) nie pomaga z limitem dziennym:

PT daje gwarantowany throughput per minute, ale nie zwiększa limitu dziennego na on-demand. To osobna usługa z osobnym modelem rozliczania.

Rozwiązanie

A) Natychmiastowe: fallback na alternatywny model:

Gdy główny model osiągnął limit dzienny, przełącz ruch na model o niewykorzystanym limicie:

# fallback_client.py - multi-model fallback z automatycznym przełączaniem
import boto3
import json
from botocore.exceptions import ClientError
import time

class BedrockFallbackClient:
    """Klient Bedrock z automatycznym fallback na alternatywne modele."""

    def __init__(self, region="eu-west-1"):
        self.client = boto3.client("bedrock-runtime", region_name=region)
        # Kolejność fallback: primary → secondary → tertiary
        self.models = [
            "anthropic.claude-3-5-sonnet-20241022-v2:0",  # primary
            "anthropic.claude-3-5-haiku-20241022-v1:0",   # tańszy, szybszy
            "anthropic.claude-3-haiku-20240307-v1:0",     # starszy, osobny limit
            "amazon.nova-pro-v1:0",                        # Amazon model, osobny limit
        ]
        self.throttled_models = {}  # model_id → timestamp when throttled

    def invoke(self, messages: list, system: str = "", max_tokens: int = 1024):
        """Wywołaj model z automatycznym fallback przy throttlingu."""
        for model_id in self.models:
            # Pomiń modele throttled w ostatnich 5 minutach
            if model_id in self.throttled_models:
                if time.time() - self.throttled_models[model_id] < 300:
                    continue
                else:
                    del self.throttled_models[model_id]

            try:
                response = self.client.converse(
                    modelId=model_id,
                    messages=messages,
                    system=[{"text": system}] if system else [],
                    inferenceConfig={"maxTokens": max_tokens}
                )
                return {
                    "content": response["output"]["message"]["content"][0]["text"],
                    "model_used": model_id,
                    "input_tokens": response["usage"]["inputTokens"],
                    "output_tokens": response["usage"]["outputTokens"],
                }
            except ClientError as e:
                error_code = e.response["Error"]["Code"]
                error_msg = e.response["Error"]["Message"]

                if error_code == "ThrottlingException":
                    if "tokens per day" in error_msg.lower():
                        # Dzienny limit - oznacz model jako niedostępny
                        self.throttled_models[model_id] = time.time()
                        print(f"⚠️  Model {model_id} osiągnął dzienny limit. Fallback...")
                        continue
                    elif "too many requests" in error_msg.lower():
                        # Per-minute limit - retry z backoff
                        time.sleep(2)
                        continue
                raise  # Inny błąd - propaguj

        raise RuntimeError("Wszystkie modele osiągnęły limit dzienny. Brak dostępnych fallback.")


# Użycie:
client = BedrockFallbackClient()
result = client.invoke(
    messages=[{"role": "user", "content": [{"text": "Wyjaśnij architekturę RAG."}]}],
    system="Jesteś ekspertem od architektury chmurowej."
)
print(f"Model: {result['model_used']}, Tokeny: {result['input_tokens']}+{result['output_tokens']}")

B) Monitoring i alerty - reaguj ZANIM osiągniesz limit:

# cloudwatch-alarm.yaml (CloudFormation)
Resources:
  BedrockDailyTokenAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: bedrock-daily-token-usage-80pct
      AlarmDescription: "Bedrock token usage at 80% of daily limit"
      Namespace: AWS/Bedrock
      MetricName: InputTokenCount
      Dimensions:
        - Name: ModelId
          Value: anthropic.claude-3-5-sonnet-20241022-v2:0
      Statistic: Sum
      Period: 86400  # 24h
      EvaluationPeriods: 1
      # Ustaw threshold na 80% Twojego dziennego limitu
      # Np. limit = 2M tokenów → threshold = 1,600,000
      Threshold: 1600000
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref SNSAlertTopic
# token_budget_tracker.py - śledzenie budżetu dziennego w aplikacji
import boto3
from datetime import datetime, timezone
import os

class TokenBudgetTracker:
    """Śledzi zużycie tokenów i ostrzega przed osiągnięciem limitu."""

    def __init__(self, daily_limit: int = 2_000_000):
        self.daily_limit = daily_limit
        self.cloudwatch = boto3.client("cloudwatch")
        self._cache_timestamp = None
        self._cached_usage = 0

    def get_daily_usage(self, model_id: str) -> int:
        """Pobierz bieżące dzienne zużycie tokenów z CloudWatch."""
        now = datetime.now(timezone.utc)
        start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)

        # Cache na 5 minut
        if self._cache_timestamp and (now - self._cache_timestamp).seconds < 300:
            return self._cached_usage

        input_resp = self.cloudwatch.get_metric_statistics(
            Namespace="AWS/Bedrock",
            MetricName="InputTokenCount",
            Dimensions=[{"Name": "ModelId", "Value": model_id}],
            StartTime=start_of_day,
            EndTime=now,
            Period=86400,
            Statistics=["Sum"],
        )
        output_resp = self.cloudwatch.get_metric_statistics(
            Namespace="AWS/Bedrock",
            MetricName="OutputTokenCount",
            Dimensions=[{"Name": "ModelId", "Value": model_id}],
            StartTime=start_of_day,
            EndTime=now,
            Period=86400,
            Statistics=["Sum"],
        )

        input_tokens = input_resp["Datapoints"][0]["Sum"] if input_resp["Datapoints"] else 0
        output_tokens = output_resp["Datapoints"][0]["Sum"] if output_resp["Datapoints"] else 0

        self._cached_usage = int(input_tokens + output_tokens)
        self._cache_timestamp = now
        return self._cached_usage

    def remaining_budget(self, model_id: str) -> dict:
        """Sprawdź ile tokenów zostało do limitu."""
        used = self.get_daily_usage(model_id)
        remaining = max(0, self.daily_limit - used)
        pct_used = (used / self.daily_limit) * 100
        return {
            "used": used,
            "remaining": remaining,
            "limit": self.daily_limit,
            "pct_used": round(pct_used, 1),
            "should_fallback": pct_used > 80,
        }

C) Zwiększenie limitu - Service Quotas request:

# 1. Znajdź quota code dla Twojego modelu
aws service-quotas list-service-quotas \
  --service-code bedrock \
  --query 'Quotas[?contains(QuotaName, `token`) && contains(QuotaName, `day`)].{name:QuotaName, code:QuotaCode, value:Value}' \
  -o table

# 2. Sprawdź aktualny limit
aws service-quotas get-service-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX

# 3. Złóż wniosek o zwiększenie (np. 2M → 10M tokenów/dzień)
aws service-quotas request-service-quota-increase \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --desired-value 10000000

# 4. Sprawdź status wniosku
aws service-quotas list-requested-service-quota-change-history-by-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --query 'RequestedQuotas[0].{status:Status, requested:DesiredValue, created:Created}'

D) Optymalizacja zużycia tokenów:

# Redukcja input tokens - kluczowe techniki

# 1. Skróć system prompt (największy wpływ)
# PRZED: 4000 tokenów system prompt × 1000 zapytań = 4M tokenów/dzień
# PO: 800 tokenów (zwięzły) × 1000 zapytań = 800K tokenów/dzień

# 2. Prompt caching (Bedrock wspiera dla Claude 3.5+)
# System prompt wysyłany raz, cache'owany przez 5 minut
response = client.converse(
    modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=messages,
    system=[{
        "text": long_system_prompt,
        "cacheControl": {"type": "ephemeral"}  # Cache na 5 min
    }],
)
# Cache hit = 90% redukcja kosztów i zużycia tokenów dla system prompt

# 3. Ogranicz max_tokens w odpowiedzi
# Nie ustawiaj max_tokens=4096 jeśli potrzebujesz 200-tokenowej odpowiedzi
response = client.converse(
    modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=messages,
    inferenceConfig={"maxTokens": 512}  # Nie 4096
)

# 4. Filtruj kontekst RAG - wysyłaj tylko top 3 chunks, nie top 10
# PRZED: 10 chunks × 500 tokenów = 5000 tokenów kontekstu
# PO: 3 chunks × 500 tokenów = 1500 tokenów kontekstu

E) Multi-region routing - podwój limit:

# Bedrock limity są per-region. Użyj drugiego regionu jako overflow.
import boto3
import random

class MultiRegionBedrockClient:
    """Rozkłada ruch między regionami aby podwoić efektywny limit."""

    def __init__(self, model_id: str):
        self.model_id = model_id
        self.regions = ["eu-west-1", "us-east-1"]  # Uwaga na data residency!
        self.clients = {
            region: boto3.client("bedrock-runtime", region_name=region)
            for region in self.regions
        }
        self.region_index = 0

    def invoke(self, messages, system="", max_tokens=1024):
        """Round-robin między regionami."""
        for attempt in range(len(self.regions)):
            region = self.regions[self.region_index % len(self.regions)]
            self.region_index += 1
            try:
                response = self.clients[region].converse(
                    modelId=self.model_id,
                    messages=messages,
                    system=[{"text": system}] if system else [],
                    inferenceConfig={"maxTokens": max_tokens}
                )
                return response
            except self.clients[region].exceptions.ThrottlingException as e:
                if "tokens per day" in str(e):
                    continue  # Spróbuj następny region
                raise
        raise RuntimeError("Limit dzienny osiągnięty we wszystkich regionach")

Walidacja

# 1. Sprawdź bieżące zużycie po wdrożeniu optymalizacji
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name InputTokenCount \
  --dimensions Name=ModelId,Value=anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 300 \
  --statistics Sum
# Expected: wartości niższe niż przed optymalizacją (po punkcie D)

# 2. Zweryfikuj że fallback działa (test z niepoprawnym primary model)
python -c "
from fallback_client import BedrockFallbackClient
client = BedrockFallbackClient()
# Symuluj throttling primary modelu
client.throttled_models['anthropic.claude-3-5-sonnet-20241022-v2:0'] = __import__('time').time()
result = client.invoke(messages=[{'role':'user','content':[{'text':'test'}]}])
print(f'Fallback model: {result[\"model_used\"]}')
assert 'haiku' in result['model_used'] or 'nova' in result['model_used']
"

# 3. Sprawdź status quota increase request
aws service-quotas list-requested-service-quota-change-history-by-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --query 'RequestedQuotas[0].Status'
# Expected: "APPROVED" (może zająć 1-3 dni robocze)

# 4. Zweryfikuj alarm CloudWatch
aws cloudwatch describe-alarms \
  --alarm-names bedrock-daily-token-usage-80pct \
  --query 'MetricAlarms[0].{state:StateValue, threshold:Threshold}'
# Expected: StateValue = "OK" (jeśli nie jesteś blisko limitu)

# 5. Po resecie o północy UTC - zweryfikuj normalną pracę
aws cloudwatch get-metric-statistics \
  --namespace AWS/Bedrock \
  --metric-name Invocation4xxErrors \
  --dimensions Name=ModelId,Value=anthropic.claude-3-5-sonnet-20241022-v2:0 \
  --start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S) \
  --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
  --period 300 \
  --statistics Sum
# Expected: 0 (brak błędów 4xx po resecie limitu)
Dzienny limit tokenów w Bedrock to twardy limit - nie ma sposobu na jego ominięcie poza zwiększeniem quoty lub rozłożeniem ruchu na wiele regionów/modeli. Dla aplikacji produkcyjnych: (1) zawsze implementuj fallback na alternatywny model, (2) monitoruj zużycie i alertuj przy 80%, (3) złóż wniosek o zwiększenie limitu (quota increase) ZANIM osiągniesz limit w produkcji. Czas rozpatrzenia wniosku to 1-3 dni robocze - nie czekaj na incydent.

 

Jerzy Kopaczewski

Bedrock throttluje Twoją aplikację AI?

Umów bezpłatną 30-minutową rozmowę. Zaprojektujemy architekturę z fallback multi-model, prompt caching i monitoringiem budżetu tokenów, aby Twoja aplikacja nigdy nie stanęła z powodu limitów.