How to Stream a Large File to S3 With AWS S3 Multipart Upload

An uploader that calls s3.put_object(Body=open(path, "rb").read()) reads the entire file into RAM before sending - it works for KB files and OOMs on anything past ~50MB. The fix is one line: s3.upload_file(path, BUCKET, key). boto3's transfer manager splits the file into multipart chunks above an 8MB threshold, uploads them in parallel, and keeps memory flat no matter how big the file is.

Data Engineerawss3multipart

Why put_object OOMs on large files

The naive uploader loads the whole file into memory before it sends a single byte:

def upload(path: str, key: str) -> None:
    with open(path, "rb") as f:
        s3.put_object(Bucket=BUCKET, Key=key, Body=f.read())

f.read() returns the entire file as one bytes object in RAM. A 12MB file needs 12MB of memory; a 4GB file needs 4GB. In a container with a modest memory limit, the process is killed by the OOM killer long before the upload finishes. There is a hard ceiling too: a single put_object cannot exceed 5GB, so above that the call fails outright regardless of memory.

The right tool is S3's multipart upload - split the object into parts, upload each part independently, and let S3 stitch them back together. You do not have to orchestrate that by hand.

Fix: use upload_file, the transfer manager

boto3 ships a high-level transfer manager. Replace the whole body of upload with one line:

def upload(path: str, key: str) -> None:
    s3.upload_file(path, BUCKET, key)

upload_file reads the file from disk in chunks - it never materializes the whole thing in memory. Above the default 8MB threshold it automatically switches to S3's multipart API: it splits the file into parts, uploads them concurrently, retries any part that fails, and completes the multipart upload for you. Memory use stays flat whether the file is 12MB or 12GB.

The full working uploader:

import os
import boto3

BUCKET = "lake"
s3 = boto3.client(
    "s3",
    endpoint_url=os.environ.get("AWS_ENDPOINT_URL"),
    region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
)

def upload(path: str, key: str) -> None:
    s3.upload_file(path, BUCKET, key)

if __name__ == "__main__":
    upload("/workspace/sample.bin", "uploads/sample.bin")
    print("uploaded sample.bin")

Run it and confirm the object landed at full size:

python3 uploader.py
aws s3api head-object --bucket lake --key uploads/sample.bin \
  --query ContentLength --output text
# -> 12582912   (matches the local 12MB file, byte for byte)

Tune multipart with TransferConfig

The defaults are good, but you can control the multipart behavior when you need to. TransferConfig sets the threshold at which multipart kicks in, the chunk size for each part, and how many parts upload in parallel:

from boto3.s3.transfer import TransferConfig

config = TransferConfig(
    multipart_threshold=8 * 1024 * 1024,   # switch to multipart above 8MB
    multipart_chunksize=8 * 1024 * 1024,   # 8MB per part
    max_concurrency=4,                     # 4 parts in flight at once
)
s3.upload_file(path, BUCKET, key, Config=config)

Larger max_concurrency speeds up big transfers on a fat network link; a larger multipart_chunksize cuts the part count (S3 allows up to 10,000 parts per object). The point is you rarely need to touch these - s3.upload_file(path, BUCKET, key) gets multipart, streaming, parallelism, and retries right by default, which is exactly why "read the whole file into memory" is the wrong reflex and this one-liner is the right one.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

How do I upload a large file to S3 in Python without running out of memory?

Use s3.upload_file(path, bucket, key) instead of s3.put_object(Body=f.read()). upload_file reads the file from disk in chunks and never loads the whole file into memory, so RAM use stays flat regardless of file size.

What is AWS S3 multipart upload and when does boto3 use it?

Multipart upload splits an object into parts that upload independently and are reassembled by S3. boto3's upload_file switches to multipart automatically once a file passes the default 8MB threshold, uploading parts in parallel and retrying any that fail.

What is the difference between put_object and upload_file in boto3?

put_object sends a single request whose Body you must supply in full - typically by reading the whole file into memory, and it caps at 5GB. upload_file is the high-level transfer manager that streams from disk in chunks and uses multipart upload above 8MB, so it handles files of any size with constant memory.

How do I change the S3 multipart chunk size in boto3?

Pass a TransferConfig to upload_file - for example TransferConfig(multipart_chunksize=8*1024*1024, multipart_threshold=8*1024*1024, max_concurrency=4). It controls the part size, the size at which multipart kicks in, and how many parts upload concurrently.

Keep learning

Back Up Postgres to AWS S3Data projectBatch Load a CSV into DynamoDBData projectConvert CSV to ParquetData projectData roadmapStep by step to hiredData interview questionsSTAR answersAll Data projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →