Large transfers, multipart and progress

boto3 · Advanced · 12 min

Big files (datasets, backups, model weights) need parallelism, resumability and progress reporting to be practical.

upload_file, upload_fileobj, download_file and download_fileobj use the S3 transfer manager (s3transfer). Above a size threshold it switches to multipart: the file is split into parts uploaded in parallel threads, each retried independently, then S3 assembles them. Downloads use parallel ranged GETs.

boto3.s3.transfer.TransferConfig tunes it:

  • multipart_threshold (default 8 MB): size at which multipart kicks in.
  • multipart_chunksize (default 8 MB): part size. S3 allows at most 10,000 parts, so for very large objects raise it (a 1 TB file needs parts of at least ~105 MB).
  • max_concurrency (default 10): parallel threads per transfer; use_threads=False disables threading.

Pass Config=TransferConfig(...) and Callback=fn; the callback receives the number of bytes transferred since the last call, from worker threads, so make it thread-safe.

Operational notes:

  • Failed multipart uploads leave incomplete parts that are billed until aborted. Add a lifecycle rule AbortIncompleteMultipartUpload to every bucket.
  • Multipart objects get an ETag that isn't the file's MD5; use checksums (ChecksumAlgorithm="SHA256" in ExtraArgs) for integrity checks.
  • Installing boto3[crt] enables the AWS Common Runtime transfer client, which can be much faster on high-bandwidth machines.

Example

import os, threading
import boto3
from boto3.s3.transfer import TransferConfig

s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="demo-big-2026")
with open("big.bin", "wb") as f:
    f.write(os.urandom(30 * 1024 * 1024))           # 30 MB test file

class Progress:
    def __init__(self, size):
        self.size, self.seen, self.lock = size, 0, threading.Lock()
    def __call__(self, nbytes):                      # called from worker threads
        with self.lock:
            self.seen += nbytes

cfg = TransferConfig(multipart_threshold=8 * 1024**2, multipart_chunksize=8 * 1024**2,
                     max_concurrency=4)
p = Progress(os.path.getsize("big.bin"))
s3.upload_file("big.bin", "demo-big-2026", "data/big.bin", Config=cfg, Callback=p,
               ExtraArgs={"ChecksumAlgorithm": "SHA256"})
print(p.seen == p.size)                               # True
etag = s3.head_object(Bucket="demo-big-2026", Key="data/big.bin")["ETag"]
print(etag.endswith('-4"'))                           # True: 4 parts, not an MD5

Remember

  • upload_file/download_file use multipart and parallel threads automatically
  • TransferConfig sets threshold, chunk size and concurrency
  • Progress callbacks get byte deltas from worker threads
  • Abort incomplete multipart uploads with a lifecycle rule

Watch out for

  • Using the default 8 MB chunk size for multi-terabyte objects and hitting the 10,000-part limit.

Check yourself

  1. Why is a multipart object's ETag not the MD5 of the file?

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/s3.html