There are three main ways to upload files to MinIO: the mc CLI, the Python SDK, and presigned URLs. The right choice depends first on who holds the credentials. If a server or script has direct access to MinIO, use mc or the SDK. If a browser or mobile client needs to upload directly, presigned URLs are the only option.
Uploading with the mc CLI
mc is MinIO's official CLI client. It's well suited for quick uploads from a local environment or for automation in shell scripts. Start by registering an alias for your MinIO server:
mc alias set myminio https://minio.example.com ACCESS_KEY SECRET_KEY
Then upload with mc cp:
# Upload a single file
mc cp /path/to/file.tar.gz myminio/mybucket/file.tar.gz
# Sync an entire directory
mc mirror /local/data/ myminio/mybucket/
mc mirror synchronizes a local directory with a bucket, skipping files that already exist and only uploading changed ones — making it a good fit for operational scripts that regularly push large numbers of files. The --max-workers flag controls the number of concurrent transfer threads, and --limit-upload caps upload bandwidth in MiB/s. Multipart uploads are handled automatically under the hood.
The prerequisite for mc is straightforward: the access key and secret key must be available in the execution environment. That's natural for CI/CD pipelines and backend servers, but this approach doesn't cover browser-based uploads.
Uploading with the Python SDK (minio-py)
When you need to upload to MinIO from application code, use the minio-py SDK. The two core methods are put_object and fput_object, and the difference is the input type.
fput_object takes a file path:
from minio import Minio
client = Minio(
"minio.example.com",
access_key="ACCESS_KEY",
secret_key="SECRET_KEY",
secure=True,
)
client.fput_object(
bucket_name="mybucket",
object_name="uploads/photo.jpg",
file_path="/tmp/photo.jpg",
content_type="image/jpeg",
)
put_object takes a stream and a size instead of a file path. Use it when you want to upload data directly from memory without writing to disk first:
import io
data = b"hello, minio"
client.put_object(
bucket_name="mybucket",
object_name="notes/hello.txt",
data=io.BytesIO(data),
length=len(data),
content_type="text/plain",
)
If the stream length isn't known ahead of time, pass -1 for length and specify part_size explicitly — the SDK will handle it as a multipart upload automatically.
Reach for the SDK instead of mc when the upload is tightly coupled to application logic: writing metadata to a database before uploading, or triggering downstream actions based on the upload result. If you're just moving a file, mc cp is far simpler.
Large Files: When Multipart Upload Kicks In
The constants defined in the minio-py source make the behavior clear:
MIN_PART_SIZE= 5 MB (5 × 1024 × 1024)MAX_PART_SIZE= 5 GB (5 × 1024 × 1024 × 1024)MAX_MULTIPART_COUNT= 10,000
When a file larger than 5 MB is passed to fput_object or put_object, the SDK automatically switches to multipart upload. The default part_size is the larger of object_size / 10000 and MIN_PART_SIZE.
The issue is that the default is optimized for small files. Uploading a 1 GB file with the default part_size can result in hundreds of parts, and the accumulated HTTP round-trip overhead adds up. You can control this by specifying part_size directly:
client.fput_object(
bucket_name="mybucket",
object_name="data/large.bin",
file_path="/data/large.bin",
part_size=16 * 1024 * 1024, # 16 MB per part
)
Here's how part count and effective throughput compare for a 1 GB file under the same network conditions:
| part_size | Parts (1 GB file) | Notes |
|---|---|---|
| 5 MB (minimum) | ~204 | High part count increases overhead |
| 16 MB | ~64 | Good balance for most environments |
| 64 MB | ~16 | Best for high-speed networks and very large files |
Results vary by environment, but a 20–40% difference between 16 MB and 5 MB parts is common. If you're uploading multiple files concurrently, parallelizing with ThreadPoolExecutor will have a bigger impact than tuning part_size.
If you pass a large stream to put_object without knowing the length, you may hit errors mid-upload. In that case, use length=-1 together with an explicit part_size, or switch to fput_object if you have a file path available — it's the safer option.
Presigned URLs — Client Uploads Without Server Credentials
When uploading from a browser or mobile app, routing the file through the server doubles bandwidth consumption: the data travels client → server → MinIO. Presigned URLs solve this structurally. The server issues a signed, one-time URL, and the client sends a PUT request directly to MinIO using that URL.
Server-side URL generation:
from datetime import timedelta
url = client.presigned_put_object(
bucket_name="mybucket",
object_name="uploads/user-photo.jpg",
expires=timedelta(hours=1),
)
# https://minio.example.com/mybucket/uploads/user-photo.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&...
Send this URL to the client, and the client uploads directly without any credentials:
curl -X PUT \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg \
"https://minio.example.com/mybucket/uploads/user-photo.jpg?X-Amz-Algorithm=..."
The URL encodes the target bucket, object path, expiration time, and signature. If the URL leaks before it expires, anyone can upload an arbitrary file to that path. For sensitive paths, keep the expiry short — 5 to 15 minutes. Using a UUID or random string for the object path limits the blast radius of a leaked URL to that single object. Adding a server-side check that verifies the object exists after upload lets you detect incomplete uploads as well.
Choosing the Right Method
| Scenario | Recommended approach |
|---|---|
| One-off upload from local/server, operational scripts | mc cp |
| Directory sync, batch transfers | mc mirror |
| Upload by file path from application code | fput_object |
| In-memory stream or dynamically generated data | put_object |
| Browser or mobile client uploads directly | Presigned PUT URL |
| Large files over 100 MB | fput_object + explicit part_size |
The three approaches aren't mutually exclusive. In practice, a common setup is: mc for operational scripts, the SDK for the application layer, and presigned URLs for user-facing uploads. Bumping part_size from the default to somewhere in the 16–64 MB range makes a noticeable difference for large file uploads, so if you're regularly pushing files over 1 GB, it's worth benchmarking directly.