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.
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
- Swapping put_object(f.read()) for s3.upload_file so the upload streams instead of buffering in RAM
- Understanding when boto3 switches to S3 multipart upload (the default 8MB threshold)
- Tuning multipart threshold, chunk size, and concurrency with TransferConfig
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
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 →