boto3 upload_file vs put_object vs upload_fileobj: Which to Use, Progress, Speed, and Fix S3 Files That Download Instead of Opening (Content-Type)
In boto3, upload_file uploads a file from disk using the managed transfer layer: it switches to multipart upload automatically above 8 MB, uploads parts in parallel, retries failed parts, can report progress, and works for files of any size, but returns nothing. upload_fileobj does the same from an open file or in-memory stream. put_object is a single API call: it sends bytes or a file object in one request (up to 5 GB), returns the response (ETag, version ID), and is the simplest choice for small content you already have in memory. Rule of thumb: files on disk, use upload_file; small strings or bytes, use put_object. One trap catches both: boto3 does not set Content-Type for you, so uploaded HTML, images and PDFs are stored as binary/octet-stream and download instead of opening in the browser; pass ContentType (in ExtraArgs for upload_file). Below: code for each method, ExtraArgs and TransferConfig, progress bars, content types, metadata and encryption, presigned uploads, errors, and the same choices in the CLI and JavaScript.
Jake built a small script to publish the shop's weekly newsletter as an HTML page in S3. The upload worked, the link worked, and clicking it downloaded a file called newsletter.html instead of showing the page. Ethan checked the object in the console: content type binary/octet-stream. Nothing was wrong with the HTML; S3 had simply been told nothing about what kind of file it was, so it served it as an anonymous blob and the browser played safe. One extra argument fixed it, and choosing the right upload method fixed the next problem, a 6 GB backup that would not go up at all.
New to boto3 or S3 permissions? The cards below cover boto3 errors, the 5 GB limit, bucket access and multipart uploads.
upload_file vs upload_fileobj vs put_object
upload_file | upload_fileobj | put_object | |
|---|---|---|---|
| Input | A file path | An open binary file or stream | Bytes, a string or a file object |
| Size | Any (multipart above 8 MB by default) | Any (multipart as needed) | Up to 5 GB in one request |
| Parallel parts and retries | Yes | Yes | No (one request, normal retries) |
| Progress callback | Yes | Yes | No |
| Returns | None | None | Response with ETag, VersionId |
| Options | ExtraArgs={...} | ExtraArgs={...} | Direct parameters (ContentType=...) |
| Errors raised | S3UploadFailedError | S3UploadFailedError | ClientError |
| Best for | Files on disk, especially large ones | Streams, uploaded web files, in-memory buffers | Small generated content: JSON, text, thumbnails |
Under the hood, upload_file and upload_fileobj call put_object for small files and the multipart APIs for big ones. That is why they handle any size, and why they hand back nothing: there may have been dozens of requests. If you need the ETag or version ID of what you just uploaded, either use put_object or call head_object afterwards.
Jake: "So upload_file is put_object with a forklift?"
Ethan: "For big loads, yes. For a sandwich, you just carry it."
Picking the method in four questions:
- Is the data a file on disk? Use
upload_file. - Is it an open stream or a web upload you do not want to save first? Use
upload_fileobj. - Is it small content you built in memory (JSON, text, a thumbnail)? Use
put_object. - Do you need the response (ETag, VersionId, checksum) right away? Prefer
put_objectfor small items, or follow a managed upload withhead_object.
upload_file: files on disk
import boto3
s3 = boto3.client("s3")
# Simplest form
s3.upload_file("reports/october.pdf", "shop-data", "reports/2026/october.pdf")
# With content type, metadata and encryption
s3.upload_file(
"site/newsletter.html", "shop-site", "newsletter.html",
ExtraArgs={
"ContentType": "text/html; charset=utf-8",
"CacheControl": "max-age=300",
"Metadata": {"issue": "42"},
"ServerSideEncryption": "aws:kms",
},
)
The arguments are positional in this order: the local file name, the bucket, the key. Mixing up the bucket and the key, or passing s3://bucket/... as the bucket, is a very common first error; our InvalidBucketName guide shows what that looks like. ExtraArgs accepts the same options as the S3 upload APIs (content type, metadata, encryption, storage class, ACL where allowed, tagging, checksums), and rejects unknown keys with an error naming the allowed ones.
upload_fileobj: streams and in-memory files
# An open file (binary mode)
with open("backups/db-backup.sql.gz", "rb") as f:
s3.upload_fileobj(f, "shop-backups", "nightly/db-backup.sql.gz")
# An in-memory buffer, for example a generated image
import io
buf = io.BytesIO(png_bytes)
s3.upload_fileobj(buf, "shop-site", "img/chart.png", ExtraArgs={"ContentType": "image/png"})
# A file uploaded to your web app (Flask/Django give you a file-like object)
s3.upload_fileobj(request.files["photo"], "shop-uploads", f"photos/{uuid4()}.jpg",
ExtraArgs={"ContentType": request.files["photo"].mimetype})
The object must be opened in binary mode ("rb"); passing a text-mode file raises an error about strings versus bytes. Like upload_file, it switches to multipart for large streams, so it is the right tool for piping a big upload through your app without saving it to disk first.
put_object: small content in one call
import json
resp = s3.put_object(
Bucket="shop-data",
Key="prices/today.json",
Body=json.JSONEncoder().encode(prices), # str or bytes
ContentType="application/json",
)
print(resp["ETag"], resp.get("VersionId"))
# Text
s3.put_object(Bucket="shop-data", Key="notes/readme.txt", Body="Hello S3", ContentType="text/plain")
# An empty "folder" marker (a zero-byte key ending in /)
s3.put_object(Bucket="shop-data", Key="exports/2026/")
put_object is one HTTP request: quick for small objects, no extra machinery, and it tells you exactly what was stored (ETag, version, checksum). Past 5 GB it fails with EntityTooLarge, covered in our EntityTooLarge guide; for anything that big, use upload_file. A single large request also restarts from zero if the connection drops, which is another reason to keep it for small content.
Files download instead of opening: set Content-Type
S3 stores whatever Content-Type you send with the upload and returns it to browsers. boto3 does not guess it from the file name, so without one the object is stored as binary/octet-stream, and browsers download the file instead of displaying it. The AWS CLI's aws s3 cp does guess from the extension, which is why the same file can behave differently depending on how it was uploaded.
The fix, in code that works for any file:
import mimetypes
def upload(path, bucket, key):
ctype, _ = mimetypes.guess_type(path)
extra = {"ContentType": ctype or "application/octet-stream"}
if ctype and ctype.startswith("text/"):
extra["ContentType"] += "; charset=utf-8"
s3.upload_file(path, bucket, key, ExtraArgs=extra)
upload("site/newsletter.html", "shop-site", "newsletter.html") # text/html; charset=utf-8
The content types you will set most often:
| File | ContentType | Browser behavior |
|---|---|---|
| .html | text/html; charset=utf-8 | Renders the page |
| .css / .js | text/css / text/javascript | Applied by pages; wrong types are blocked |
| .json | application/json | Shown or parsed by fetch() |
application/pdf | Opens in the built-in viewer | |
| .jpg / .png / .webp / .svg | image/jpeg / image/png / image/webp / image/svg+xml | Displays the image |
| .mp4 | video/mp4 | Plays in the browser |
| Anything, forced download | Any type + ContentDisposition="attachment" | Saves to disk |
Fixing objects that are already uploaded: copy each object onto itself with the new type and MetadataDirective="REPLACE":
s3.copy_object(Bucket="shop-site", Key="newsletter.html",
CopySource={"Bucket": "shop-site", "Key": "newsletter.html"},
ContentType="text/html; charset=utf-8", MetadataDirective="REPLACE")
With REPLACE, any custom metadata on the object is replaced too, so pass it again if you had some. For a whole bucket of wrongly typed files:
- List the objects with a paginator over
list_objects_v2. - Guess each type from the key with
mimetypes.guess_type. - Check the current type with
head_objectand skip objects that are already right. - Copy in place with the new
ContentTypeandMetadataDirective="REPLACE", carrying over existing metadata. - Invalidate the CDN cache if the files are served through CloudFront, so visitors get the new headers.
In the console you can also change it per object: select it, choose Actions, Edit metadata, and set Content-Type. The opposite case, forcing a download for a file that would normally open, uses ContentDisposition="attachment; filename=report.pdf".
Jake: "So the HTML was fine, S3 just didn't know it was HTML?"
Ethan: "Exactly. It stores the label you give it, and boto3 doesn't write labels for you."
Progress bars and speed: Callback and TransferConfig
import os, sys, threading
from boto3.s3.transfer import TransferConfig
class Progress:
def __init__(self, path):
self.size = os.path.getsize(path); self.seen = 0; self.lock = threading.Lock()
def __call__(self, n):
with self.lock:
self.seen += n
sys.stdout.write(f"\r{self.seen / self.size:6.1%}"); sys.stdout.flush()
config = TransferConfig(
multipart_threshold=64 * 1024 * 1024, # switch to multipart above 64 MB
multipart_chunksize=64 * 1024 * 1024, # part size
max_concurrency=10, # parallel threads
)
path = "backups/full-6gb.tar.gz"
s3.upload_file(path, "shop-backups", "weekly/full.tar.gz", Config=config, Callback=Progress(path))
The callback receives the number of bytes sent since the last call, from several threads, which is why it uses a lock. For fast networks, raising max_concurrency and the chunk size speeds up very large uploads; on slow or shared connections, lowering concurrency avoids timeouts. Our RequestTimeout guide covers uploads that stall.
Interrupted big uploads leave unfinished multipart uploads behind, which are billed as storage until aborted. A lifecycle rule that aborts incomplete multipart uploads after a few days keeps them from piling up; our NoSuchUpload guide explains how to find and clear them.
Metadata, encryption, storage class and checksums
- Metadata:
"Metadata": {"issue": "42"}becomesx-amz-meta-issue; keep values short ASCII strings. - Encryption: buckets encrypt by default with SSE-S3;
"ServerSideEncryption": "aws:kms"plus"SSEKMSKeyId"uses your KMS key, which the uploading role must be allowed to use. - Storage class:
"StorageClass": "STANDARD_IA"or"GLACIER_IR"for backups you rarely read. - Tagging:
"Tagging": "project=shop&team=ops"for lifecycle rules and cost reports. - Checksums:
"ChecksumAlgorithm": "SHA256"(or CRC32) asks S3 to verify integrity end to end; current boto3 versions add a default checksum on uploads automatically. - ACLs: most buckets now have ACLs disabled (Bucket owner enforced), so passing
"ACL": "public-read"fails; use a bucket policy instead, as our bucket access guide shows.
Letting a browser upload directly: presigned URLs
For user uploads, a common pattern is to let the browser send the file straight to S3, so it never passes through your server. Your server signs a short-lived URL:
# A presigned PUT: the browser sends the file body to this URL
url = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "shop-uploads", "Key": "photos/abc123.jpg", "ContentType": "image/jpeg"},
ExpiresIn=600,
)
# Or a presigned POST, which can limit size and type with conditions
post = s3.generate_presigned_post(
"shop-uploads", "photos/abc123.jpg",
Conditions=[["content-length-range", 1, 10_000_000], {"Content-Type": "image/jpeg"}],
ExpiresIn=600,
)
If the URL is signed with a ContentType, the browser must send exactly that header, or S3 rejects it with SignatureDoesNotMatch (our SignatureDoesNotMatch guide covers it). The bucket also needs a CORS rule allowing PUT or POST from your site.
Jake: "So our server never touches the photo itself?"
Ethan: "It just signs a permission slip. The photo goes straight from the customer's phone to S3."
Upload errors, fixed
S3UploadFailedError: Failed to upload ... An error occurred (AccessDenied): the role lackss3:PutObjecton that key, or cannot use the KMS key for SSE-KMS. Catchboto3.exceptions.S3UploadFailedErrorforupload_file, andbotocore.exceptions.ClientErrorforput_object; our boto3 error handling guide shows both.EntityTooLargefrom put_object: over 5 GB in one request; switch toupload_file.AccessControlListNotSupported: you passed an ACL to a bucket with ACLs disabled; remove it.ValueError: Invalid extra_args key: a misspelled or unsupported option inExtraArgs; the message lists the allowed keys.TypeErrorabout str and bytes: a text-mode file passed toupload_fileobj; open it with"rb".FileNotFoundError: the local path is relative to where Python runs, not where the script lives; build it withpathlibfrom__file__.- Upload "works" but the file opens wrongly: missing or wrong Content-Type, as above.
The same choices in the CLI and JavaScript
- AWS CLI:
aws s3 cp file s3://bucket/keybehaves likeupload_file(multipart, parallel, content type guessed);aws s3api put-object --bucket b --key k --body filebehaves likeput_object. Add--content-typeto either to set the type yourself. - JavaScript SDK v3:
PutObjectCommandis the single request; theUploadclass from@aws-sdk/lib-storageis the managed, multipart, parallel version with progress events. Reading files back is covered in our GetObject guide.
Whichever tool you use, the same two decisions come up: one request or a managed multipart transfer, and which content type to store. Make both deliberately, and uploads behave the same from your script, your CI pipeline and your web app.
Frequently asked questions
What is the difference between upload_file and put_object in boto3?
upload_file uploads a file from disk with automatic multipart, parallel parts, retries and progress, for any size; put_object sends content in one request up to 5 GB and returns the response.
When should I use put_object instead of upload_file?
For small content you already have in memory, such as JSON or text, or when you need the ETag or version ID from the response.
What is upload_fileobj in boto3?
The managed upload for an open binary file or stream, such as an in-memory buffer or a file uploaded to a web app.
Why does my S3 file download instead of opening in the browser?
Its Content-Type is binary/octet-stream because boto3 does not set one. Pass ContentType when uploading, or update it with copy_object and MetadataDirective REPLACE.
How do I set Content-Type with upload_file?
Pass ExtraArgs={"ContentType": "text/html"}, or guess it with mimetypes.guess_type.
Does upload_file use multipart upload?
Yes, automatically, for files above the multipart threshold (8 MB by default), with parts uploaded in parallel.
What is the maximum size for put_object?
5 GB in a single request; use upload_file or multipart upload for larger files, up to 50 TB per object.
How do I show upload progress in boto3?
Pass a Callback function to upload_file or upload_fileobj; it receives the bytes sent since the last call.
How do I make boto3 uploads faster?
Use a TransferConfig with a larger max_concurrency and multipart_chunksize for big files on fast networks.
What does upload_file return?
None. Use put_object or head_object if you need the ETag or version ID.
How do I add metadata to an S3 upload in boto3?
Pass Metadata={"key": "value"} in ExtraArgs or to put_object; it is stored as x-amz-meta-key.
How do I upload a string to S3 with boto3?
s3.put_object(Bucket=..., Key=..., Body="text", ContentType="text/plain").
How do I upload with KMS encryption in boto3?
Pass ServerSideEncryption="aws:kms" and SSEKMSKeyId; the role must be allowed to use the key.
How do I let users upload directly to S3 from a browser?
Generate a presigned PUT URL or presigned POST on your server with boto3 and give it to the browser; add a CORS rule to the bucket.
What exception does upload_file raise?
boto3.exceptions.S3UploadFailedError, which wraps the underlying ClientError.
How do I create a folder in S3 with boto3?
Folders are key prefixes; put_object with a Key ending in a slash creates an empty marker, but uploading a key such as exports/file.csv is enough.
Does the AWS CLI set Content-Type automatically?
Yes. aws s3 cp guesses it from the file extension, unlike boto3; use --content-type to override.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. What is the largest object you can upload to S3 in a single PUT request?
5 GB; larger objects need multipart upload.
2. When should you use multipart upload for S3?
For large objects (recommended from about 100 MB): parts upload in parallel and failed parts can be retried alone.
3. Users must upload files directly from browsers to S3 without passing through the app server. What do you use?
Presigned URLs (PUT or POST) generated by the server.
4. Files uploaded to S3 download instead of displaying in the browser. What is wrong?
The objects were stored without the correct Content-Type metadata.
5. Which S3 lifecycle action removes leftover parts of failed multipart uploads?
AbortIncompleteMultipartUpload.
6. Which IAM action does an application need to upload objects to S3?
s3:PutObject (and kms:GenerateDataKey for SSE-KMS buckets).
7. How can you speed up uploads of large files to a distant S3 bucket?
Use multipart upload with parallel parts, and S3 Transfer Acceleration if the distance is the bottleneck.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
The newsletter script now guesses each file's type, so HTML pages open, images show and PDFs preview in the browser. The 6 GB weekly backup goes up with upload_file, a 64 MB chunk size and a progress bar, in a few minutes instead of failing at 5 GB. And the small price list Jake regenerates every hour uses put_object, because he wanted its version ID in his logs. Three methods, three jobs, no more guessing.
📌 If you keep one line from this page
Use upload_file for files on disk (any size, multipart, progress), upload_fileobj for streams, and put_object for small in-memory content up to 5 GB, and always pass a ContentType so browsers open your files instead of downloading them.
ExtraArgs for options · TransferConfig for speed · Callback for progress · copy_object to fix old types.
Revision note. Written October 19, 2026. S3 stores exactly what you tell it about a file, including what kind of file it is.