Config: retries, timeouts and endpoints

boto3 · Advanced · 12 min

Default timeouts and retries rarely fit production; the wrong values cause hung requests or cascading throttling.

botocore.config.Config customizes a client: boto3.client("s3", config=Config(...)). Configs can be merged with cfg.merge(other).

Retries: botocore automatically retries throttling errors, 5xx responses and transient network errors with exponential backoff. The retries dict chooses the behavior:

  • mode="legacy": the long-standing default in boto3, with service-specific quirks.
  • mode="standard": consistent rules across services, 3 total attempts by default, and a retry quota that stops retry storms. AWS recommends it for new code.
  • mode="adaptive": standard plus client-side rate limiting that slows down when throttled; useful for single-process batch jobs hammering one API.

Mind the counting: in Config, total_max_attempts includes the first call, while the older max_attempts key counts retries only (so max_attempts=3 means up to 4 calls). The AWS_MAX_ATTEMPTS variable and the max_attempts profile setting count total attempts. The mode can also come from AWS_RETRY_MODE.

Timeouts: connect_timeout and read_timeout default to 60 seconds each, far too long for request-serving code: a stuck call ties up a worker for minutes once retries multiply it. Set them to what your latency budget allows.

Connection pool: max_pool_connections (default 10) limits concurrent connections per client; raise it when many threads share one client.

Endpoints: endpoint_url= on the client targets LocalStack, MinIO, VPC endpoints or FIPS endpoints. Other options include region_name, signature_version, s3={"addressing_style": "path"} and user_agent_extra.

Example

import boto3
from botocore.config import Config

fast_api = Config(
    region_name="us-east-1",
    retries={"mode": "standard", "total_max_attempts": 4},   # 1 call + up to 3 retries
    connect_timeout=2,
    read_timeout=5,
    max_pool_connections=50,
)
ddb = boto3.client("dynamodb", config=fast_api)
print(ddb.meta.config.retries)            # {'mode': 'standard', 'total_max_attempts': 4}
print(ddb.meta.config.read_timeout)       # 5

batch = fast_api.merge(Config(retries={"mode": "adaptive", "total_max_attempts": 10}))
print(batch.retries["mode"], batch.connect_timeout)   # adaptive 2

# Local S3-compatible storage (MinIO / LocalStack):
minio = boto3.client("s3", endpoint_url="http://localhost:9000",
                     config=Config(s3={"addressing_style": "path"}))
print(minio.meta.endpoint_url)            # http://localhost:9000

Remember

  • Config(retries={'mode': 'standard', 'total_max_attempts': N}) controls retries
  • In Config, total_max_attempts includes the first call; max_attempts counts retries only
  • Default 60 s connect/read timeouts are too long for request paths
  • endpoint_url targets LocalStack, MinIO or VPC endpoints

Watch out for

  • Wrapping boto3 calls in your own retry loop on top of botocore's retries, multiplying attempts and latency.

Check yourself

  1. With Config(retries={'max_attempts': 3}), how many calls can a throttled operation make in total?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: boto3.amazonaws.com/v1/documentation/api/latest/guide/retries.html