AWS Bedrock throttling GenAI quota

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

Fixing ThrottlingException 'too many tokens per day' in AWS Bedrock: analysing daily limits, retry with exponential backoff, requesting quota increase, fallback to alternative models.

Jerzy Kopaczewski ·
An application using AWS Bedrock (InvokeModel / Converse API) starts returning HTTP 429 with the message: ThrottlingException: Too many tokens per day, please wait before trying again. The problem appears after reaching the daily token limit for a given model (e.g. Claude, Llama). All subsequent requests are rejected until the limit resets at midnight UTC. The application becomes non-functional for the rest of the day.

This runbook covers diagnosing and working around the daily token limit in AWS Bedrock. We discuss inference architecture on AWS and cost comparison in the article LLM Inference on AWS - Bedrock vs SageMaker vs Self-Hosted on EKS. Building a RAG pipeline with Knowledge Base is covered in RAG Architecture on AWS. If you need help optimising Bedrock costs and limits - book a consultation.

Symptom

Bedrock API returns a 429 error with a message indicating the daily token limit has been reached:

# Typical error message in application logs:
# botocore.exceptions.ClientError: An error occurred (ThrottlingException) when calling
# the InvokeModel operation: Too many tokens per day, please wait before trying again.

# Message variants:
# "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"

# Check CloudWatch Bedrock metrics - token consumption
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)'

# Check OutputTokenCount similarly
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)'

# Check Invocation4xxErrors metric (counts 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

# Check current service quota for the model
aws service-quotas get-service-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --query '{quotaName:QuotaName, value:Value, unit:Unit}'

Key distinction - Bedrock has three types of limits:

Limit type Message Reset Solution
Tokens per minute (TPM) “Too many requests” / rate limit After 60s Retry with backoff
Requests per minute (RPM) “Too many requests” After 60s Retry with backoff
Tokens per day (TPD) “Too many tokens per day” Midnight UTC This runbook

Root Cause

AWS Bedrock imposes three layers of limits on each model, per region, per account:

1. Default daily limit is low for new accounts:

Newly activated models have low default daily limits. For example, Claude 3.5 Sonnet on a new account may have a limit of 1M tokens/day. With an average RAG query (~4K input + 1K output = 5K tokens), that is only 200 queries per day.

2. Input tokens + output tokens count toward the limit:

The “tokens per day” limit is the sum of input tokens (prompt) + output tokens (completion). Long system prompts (typical in RAG: 2-8K tokens) consume the limit faster than expected.

3. No visible warning before reaching the limit:

Bedrock does not return a header with remaining limit information (unlike OpenAI API). The only way to monitor usage is through CloudWatch metrics. Without an alert, the application suddenly stops working.

4. Reset at midnight UTC - not local midnight:

The limit resets at 00:00 UTC. For a team in CET (UTC+1/+2), this means a reset at 01:00-02:00 local time. An application hitting the limit at 4:00 PM local time is non-functional for 9-10 hours.

5. Provisioned Throughput (PT) does not help with the daily limit:

PT provides guaranteed per-minute throughput, but does not increase the daily limit for on-demand usage. It is a separate service with a separate billing model.

Solution

A) Immediate: fallback to an alternative model:

When the primary model hits its daily limit, route traffic to a model with unused quota:

# fallback_client.py - multi-model fallback with automatic switching
import boto3
import json
from botocore.exceptions import ClientError
import time

class BedrockFallbackClient:
    """Bedrock client with automatic fallback to alternative models."""

    def __init__(self, region="eu-west-1"):
        self.client = boto3.client("bedrock-runtime", region_name=region)
        # Fallback order: primary → secondary → tertiary
        self.models = [
            "anthropic.claude-3-5-sonnet-20241022-v2:0",  # primary
            "anthropic.claude-3-5-haiku-20241022-v1:0",   # cheaper, faster
            "anthropic.claude-3-haiku-20240307-v1:0",     # older, separate limit
            "amazon.nova-pro-v1:0",                        # Amazon model, separate limit
        ]
        self.throttled_models = {}  # model_id → timestamp when throttled

    def invoke(self, messages: list, system: str = "", max_tokens: int = 1024):
        """Invoke model with automatic fallback on throttling."""
        for model_id in self.models:
            # Skip models throttled in the last 5 minutes
            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():
                        # Daily limit - mark model as unavailable
                        self.throttled_models[model_id] = time.time()
                        print(f"⚠️  Model {model_id} hit daily limit. Falling back...")
                        continue
                    elif "too many requests" in error_msg.lower():
                        # Per-minute limit - retry with backoff
                        time.sleep(2)
                        continue
                raise  # Other error - propagate

        raise RuntimeError("All models have reached their daily limit. No fallback available.")


# Usage:
client = BedrockFallbackClient()
result = client.invoke(
    messages=[{"role": "user", "content": [{"text": "Explain RAG architecture."}]}],
    system="You are a cloud architecture expert."
)
print(f"Model: {result['model_used']}, Tokens: {result['input_tokens']}+{result['output_tokens']}")

B) Monitoring and alerts - react BEFORE hitting the 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
      # Set threshold to 80% of your daily limit
      # E.g. limit = 2M tokens → threshold = 1,600,000
      Threshold: 1600000
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref SNSAlertTopic
# token_budget_tracker.py - tracking daily budget in your application
import boto3
from datetime import datetime, timezone
import os

class TokenBudgetTracker:
    """Tracks token consumption and warns before reaching the limit."""

    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:
        """Fetch current daily token usage from CloudWatch."""
        now = datetime.now(timezone.utc)
        start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0)

        # Cache for 5 minutes
        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:
        """Check how many tokens remain before hitting the limit."""
        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) Increase limit - Service Quotas request:

# 1. Find the quota code for your model
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. Check current limit
aws service-quotas get-service-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX

# 3. Submit a request for increase (e.g. 2M → 10M tokens/day)
aws service-quotas request-service-quota-increase \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --desired-value 10000000

# 4. Check request status
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) Optimise token consumption:

# Reducing input tokens - key techniques

# 1. Shorten system prompt (biggest impact)
# BEFORE: 4000 token system prompt × 1000 queries = 4M tokens/day
# AFTER: 800 tokens (concise) × 1000 queries = 800K tokens/day

# 2. Prompt caching (Bedrock supports it for Claude 3.5+)
# System prompt sent once, cached for 5 minutes
response = client.converse(
    modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=messages,
    system=[{
        "text": long_system_prompt,
        "cacheControl": {"type": "ephemeral"}  # Cache for 5 min
    }],
)
# Cache hit = 90% reduction in cost and token usage for system prompt

# 3. Limit max_tokens in response
# Do not set max_tokens=4096 if you need a 200-token response
response = client.converse(
    modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
    messages=messages,
    inferenceConfig={"maxTokens": 512}  # Not 4096
)

# 4. Filter RAG context - send only top 3 chunks, not top 10
# BEFORE: 10 chunks × 500 tokens = 5000 tokens of context
# AFTER: 3 chunks × 500 tokens = 1500 tokens of context

E) Multi-region routing - double your limit:

# Bedrock limits are per-region. Use a second region as overflow.
import boto3
import random

class MultiRegionBedrockClient:
    """Distributes traffic across regions to double the effective limit."""

    def __init__(self, model_id: str):
        self.model_id = model_id
        self.regions = ["eu-west-1", "us-east-1"]  # Watch out for 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 between regions."""
        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  # Try next region
                raise
        raise RuntimeError("Daily limit reached in all regions")

Validation

# 1. Check current usage after deploying optimisations
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: values lower than before optimisation (after point D)

# 2. Verify fallback works (test with invalid primary model)
python -c "
from fallback_client import BedrockFallbackClient
client = BedrockFallbackClient()
# Simulate primary model throttling
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. Check quota increase request status
aws service-quotas list-requested-service-quota-change-history-by-quota \
  --service-code bedrock \
  --quota-code L-XXXXXXXX \
  --query 'RequestedQuotas[0].Status'
# Expected: "APPROVED" (may take 1-3 business days)

# 4. Verify CloudWatch alarm
aws cloudwatch describe-alarms \
  --alarm-names bedrock-daily-token-usage-80pct \
  --query 'MetricAlarms[0].{state:StateValue, threshold:Threshold}'
# Expected: StateValue = "OK" (if you are not close to the limit)

# 5. After midnight UTC reset - verify normal operation
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 (no 4xx errors after limit reset)
The daily token limit in Bedrock is a hard limit - there is no way to bypass it other than increasing the quota or distributing traffic across multiple regions/models. For production applications: (1) always implement fallback to an alternative model, (2) monitor usage and alert at 80%, (3) submit a quota increase request BEFORE you hit the limit in production. Processing time is 1-3 business days - do not wait for an incident.

 

Jerzy Kopaczewski

Bedrock throttling your AI application?

Schedule a free 30-minute call. We will design an architecture with multi-model fallback, prompt caching, and token budget monitoring so your application never stops due to limits.