boto/boto3 · error · RetriesExceededError

Max Retries Exceeded

Error message

Max Retries Exceeded

What it means

`RetriesExceededError` is raised by `S3Transfer.download_file` when the underlying s3transfer exhausts all retry attempts (each chunk/part download keeps failing). It wraps `S3TransferRetriesExceededError` for backwards compatibility, since users historically caught boto3's exception. The inner `last_exception` carries the cause of the final failed attempt. It usually points at persistent network problems or a consistently failing request (e.g. permission denied on every retry), not transient noise.

Solutions

  1. Inspect `e.last_exception` to see the real failure (ClientError code or socket error) — fixing the root cause beats raising the retry cap.
  2. Increase the retry budget: `TransferConfig(max_request_concurrency=..., max_concurrency=...)` and, for the client, set `Config(retries={'max_attempts': 10, 'mode': 'adaptive'})`.
  3. Reduce per-request size to lower the chance any single part fails: `TransferConfig(multipart_threshold=..., multipart_chunksize=...)`.
  4. For transient network issues, wrap the call in an outer retry with backoff (e.g. `tenacity`), and verify connectivity to `*.s3.<region>.amazonaws.com`.

Example fix

# before
client.download_file('bucket', 'big', '/tmp/big')  # RetriesExceededError

# after: diagnose + expand budget
from botocore.config import Config
client = boto3.client('s3', config=Config(retries={'max_attempts': 10, 'mode': 'adaptive'}))
from boto3.s3.transfer import TransferConfig
client.download_file('bucket', 'big', '/tmp/big', Config=TransferConfig(max_concurrency=20))
Defensive patterns

Strategy: retry

Validate before calling

from botocore.config import Config
from boto3.s3.transfer import TransferConfig

s3 = boto3.client('s3', config=Config(retries={'max_attempts': 10, 'mode': 'adaptive'}))
tcfg = TransferConfig(max_concurrency=20)
# inspect last_exception after a failure to decide if a retry is worthwhile

Type guard

from boto3.exceptions import RetriesExceededError
def is_retries_exceeded(e) -> bool:
    return isinstance(e, RetriesExceededError)

Try / catch

import time
from boto3.exceptions import RetriesExceededError
attempt = 0
while True:
    try:
        client.download_file(bucket, key, path, Config=tcfg)
        break
    except RetriesExceededError as e:
        attempt += 1
        if attempt > 3:
            raise
        last = getattr(e, 'last_exception', None)
        log.warning('download retry %d, last=%r', attempt, last)
        time.sleep(2 ** attempt)

Prevention

When it happens

Trigger: A `download_file` call where every attempt to fetch the object (or one of its parts in a multipart download) fails: repeated timeouts, connection resets, persistent `403 AccessDenied`, or `404 NoSuchKey` on an object that never appears during the retry window.

Common situations: Unstable/slow network (mobile, congested NAT), restrictive firewall/proxy dropping large ranged GETs, IAM permissions failing consistently, an object that was deleted mid-download, or the default retry budget being too small for a flaky link combined with a large file.

Related errors


AI-assisted analysis of boto/boto3@6e10b029c1 (2026-08-11). Data as JSON: /api/errors/97f13c304134d272. Report an issue: GitHub.

Appendix: source

Thrown at boto3/s3/transfer.py:491

        """
        if isinstance(filename, PathLike):
            filename = fspath(filename)
        if not isinstance(filename, str):
            raise ValueError('Filename must be a string or a path-like object')

        subscribers = self._get_subscribers(callback)
        future = self._manager.download(
            bucket, key, filename, extra_args, subscribers
        )
        try:
            future.result()
        # This is for backwards compatibility where when retries are
        # exceeded we need to throw the same error from boto3 instead of
        # s3transfer's built in RetriesExceededError as current users are
        # catching the boto3 one instead of the s3transfer exception to do
        # their own retries.
        except S3TransferRetriesExceededError as e:
            raise RetriesExceededError(e.last_exception)

    def _get_subscribers(self, callback):
        if not callback:
            return None
        return [ProgressCallbackInvoker(callback)]

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self._manager.__exit__(*args)


class ProgressCallbackInvoker(BaseSubscriber):
    """A back-compat wrapper to invoke a provided callback via a subscriber

    :param callback: A callable that takes a single positional argument for
        how many bytes were transferred.

View on GitHub (pinned to 6e10b029c1)