AWS

ThrottlingException: Rate exceeded

You hit an AWS API rate limit. Which limits are raiseable and which are not, how to back off correctly, and why the caller is usually a loop you did not write.

medium fix5 min read

the aws error
An error occurred (ThrottlingException) when calling the DescribeInstances operation (reached max retries: 4): Rate exceeded

An error occurred (RequestLimitExceeded) when calling the RunInstances operation: Request limit exceeded.

An error occurred (TooManyRequestsException) when calling the Invoke operation: Rate Exceeded.

ProvisionedThroughputExceededException

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

  1. 1

    Find out who is actually making the calls

    aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=DescribeInstances --max-results 20 --query 'Events[].{Time:EventTime,User:Username}' --output table

    The throttle is account-wide and per region, so the caller exhausting the budget is frequently not the thing reporting the error. Monitoring agents, autoscalers and Terraform refreshes are common culprits.

  2. 2

    Raise the SDK's own retry budget before changing anything else

    AWS_RETRY_MODE=adaptive AWS_MAX_ATTEMPTS=10 aws ec2 describe-instances --max-items 5

    Adaptive mode adds client-side rate limiting on top of exponential backoff, which is specifically designed for this. It is off by default, and turning it on resolves a lot of intermittent throttling without any code change.

  3. 3

    Check whether the quota is adjustable

    aws service-quotas list-service-quotas --service-code ec2 --query 'Quotas[?Adjustable==`true`].[QuotaName,Value]' --output table

    Some limits can be raised through Service Quotas and others cannot. API request rates are often not listed at all, which means the answer is to make fewer calls rather than to ask for more.

All 8 sections

AWS rate-limits its control plane APIs per account, per region, per service. Exceeding the budget returns a throttling error. The error names vary by service and mean the same thing:

ErrorServices
ThrottlingExceptionMost
RequestLimitExceededEC2
TooManyRequestsExceptionLambda, API Gateway
ProvisionedThroughputExceededExceptionDynamoDB
SlowDownS3

Most use a token bucket: a sustained refill rate plus a burst capacity. Bursts are absorbed, sustained excess is not.

The caller is often not you

Throttling is account-wide, so the request that fails is frequently an innocent bystander. Find the real consumer:

aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=DescribeInstances \
  --max-results 50 \
  --query 'Events[].{Time:EventTime,User:Username}' --output table

For a proper count, query CloudTrail Lake or Athena over the CloudTrail S3 bucket and group by userIdentity.arn. The usual offenders:

  • A monitoring agent polling DescribeInstances every few seconds across many accounts
  • Cluster Autoscaler or Karpenter during a scaling event
  • A Terraform refresh over hundreds of resources
  • A CI pipeline running many jobs in parallel, each with its own client
  • A retry loop with no backoff, which turns one throttle into a storm

The last is worth emphasising: naive retries make throttling worse, not better.

Turn on adaptive retries

The SDK defaults are conservative. Adaptive mode adds a client-side rate limiter that slows down when it observes throttling, which is exactly the right behaviour:

export AWS_RETRY_MODE=adaptive
export AWS_MAX_ATTEMPTS=10
# ~/.aws/config
[default]
retry_mode = adaptive
max_attempts = 10
ModeBehaviour
legacyOld default, few retries
standardExponential backoff with jitter, 3 attempts
adaptiveStandard plus client-side rate limiting

In code:

import boto3
from botocore.config import Config

cfg = Config(retries={"max_attempts": 10, "mode": "adaptive"})
ec2 = boto3.client("ec2", config=cfg)

Jitter matters. Fixed backoff synchronises every client onto the same retry schedule, so they all retry together and throttle together. The SDK's standard and adaptive modes already randomise; a hand-rolled time.sleep(2 ** n) does not.

Make fewer calls

Usually more effective than any retry tuning.

Paginate properly and ask for more per page:

paginator = ec2.get_paginator("describe_instances")
for page in paginator.paginate(PaginationConfig={"PageSize": 1000}):
    ...

Filter server-side rather than listing everything and filtering locally:

aws ec2 describe-instances \
  --filters "Name=tag:Environment,Values=production" \
  --query 'Reservations[].Instances[].InstanceId'

--filters is applied by EC2; --query is applied by the CLI after the response arrives. Only the first reduces API load.

Cache. A list of subnets or AMIs does not change every thirty seconds. Caching for five minutes can remove most of the traffic.

Use events instead of polling. EventBridge notifies you when an instance state changes, which replaces a polling loop entirely.

Quotas

aws service-quotas list-service-quotas --service-code ec2 \
  --query 'Quotas[?Adjustable==`true`].[QuotaName,Value]' --output table

aws service-quotas request-service-quota-increase \
  --service-code ec2 --quota-code L-1216C47A --quota-value 100

Two things to know. Many API rate limits are not exposed in Service Quotas at all and are not adjustable, so no amount of asking helps and the answer is to call less. And resource quotas, such as the number of running vCPUs, are a different thing from rate limits; hitting those produces a different error.

Terraform specifically

A large configuration refreshes every resource on every plan, which is a burst of API calls:

terraform plan -parallelism=5     # default is 10
terraform plan -refresh=false     # skip refresh entirely
provider "aws" {
  region     = "eu-west-1"
  max_retries = 10
}

-refresh=false is safe for a quick check and not for anything you will apply, since it plans against possibly stale state.

Splitting one enormous configuration into several smaller ones reduces the burst and is usually worth doing for other reasons too.

DynamoDB and S3 are different

ProvisionedThroughputExceededException is about a table's capacity, not the control plane. Switch the table to on-demand, or raise provisioned capacity, or fix a hot partition key.

S3 SlowDown relates to request rate per prefix. S3 scales automatically but takes time to adapt, so a sudden burst on one prefix throttles. Distributing keys across prefixes helps, and the entropy no longer needs to be at the start of the key as it once did.

A checklist

  1. CloudTrail to find the real caller. It is often not the code reporting the error.
  2. AWS_RETRY_MODE=adaptive and AWS_MAX_ATTEMPTS=10.
  3. Never hand-roll retries without jitter. Synchronised retries make it worse.
  4. Paginate with a large page size rather than many small calls.
  5. Filter server-side with --filters, not client-side with --query.
  6. Cache slow-changing data, and use EventBridge instead of polling.
  7. service-quotas to check if the limit is even adjustable. API rates often are not.
  8. Terraform → lower -parallelism, raise max_retries, split large configurations.

Frequently Asked Questions

Why am I being throttled when my application makes few API calls?

Because throttling budgets are shared across the whole account and region, not per application. The caller consuming the budget is frequently something else entirely: a monitoring agent polling DescribeInstances, a cluster autoscaler during a scaling event, a Terraform refresh across hundreds of resources, or a retry loop somewhere with no backoff. Use CloudTrail to identify who is actually making the calls before changing anything in the code that happens to be reporting the error.

Does retrying fix throttling?

Retrying with correct exponential backoff and jitter is part of the answer; retrying naively makes it worse. A tight retry loop turns one throttled request into several, and fixed backoff synchronises every client onto the same schedule so they all retry at the same instant. Use the SDK's adaptive retry mode, which adds a client-side rate limiter that slows down when it observes throttling. Set AWS_RETRY_MODE=adaptive and AWS_MAX_ATTEMPTS=10, since the defaults are conservative.

Can I get AWS to raise my API rate limit?

Sometimes. Service Quotas lists which limits are adjustable, and request-service-quota-increase submits a request for those. Many API request rates are not exposed there at all and are not adjustable, in which case the only answer is to make fewer calls. It is also worth distinguishing rate limits from resource quotas: a cap on running vCPUs is a resource quota and produces a different error from a request-rate throttle.

What is the difference between --filters and --query in the AWS CLI?

--filters is sent to the service, which returns only matching results, so it reduces both the response size and the number of paginated calls. --query is a JMESPath expression applied locally by the CLI after the full response has arrived, so it changes what you see and does nothing for API load. When you are being throttled, moving a filter from --query to --filters can substantially reduce the number of requests.

How do I reduce throttling from Terraform?

Lower -parallelism, which defaults to 10, and raise max_retries in the provider block. For a quick plan you can use -refresh=false to skip the refresh entirely, though that plans against potentially stale state and is not appropriate for anything you intend to apply. The structural fix is to split a very large configuration into smaller ones, which reduces the size of each refresh burst and tends to be worth doing for change-safety reasons anyway.

Reference and practice

Learn the underlying concept

Other AWS errors