Large files & multipart
How big files upload as parallel parts — automatically.
Large files upload as multipart: the file is split into parts that upload concurrently and directly to your bucket, then the API stitches them together. You do not configure when this happens or wire anything up — it is automatic.
When multipart kicks in
A file at or above 100 MB uploads as multipart; anything smaller is a single
PUT. The threshold is decided server-side by createUpload,
which returns either a single or multipart upload descriptor. Parts are
16 MB each.
| File size | Upload mode | Parts |
|---|---|---|
| Under 100 MB | single | One presigned PUT. |
| 100 MB and up | multipart | One presigned PUT per 16 MB part. |
How it flows
createUploadinitiates a multipart upload on the bucket and returns a presigned PUT URL for each part, plus theuploadIdandpartSize.- The uploader sends parts concurrently — 6 at a time by default
(
partConcurrency) — straight to the bucket, slicing the file locally per thepartSize. - Each part returns an
ETag. The uploader collects them and passes them tocomplete. - The API completes the multipart upload from the part ETags and confirms the object exists.
The byte path is still browser → bucket; the API only signs and stitches.
Tuning throughput
For very large files, raising partConcurrency can saturate more of the
available bandwidth. For a batch of large files, balance it against concurrency
(files in parallel) so you do not open too many sockets at once.
await uploadFiles(files, {
concurrency: 2, // 2 large files at a time
partConcurrency: 10, // 10 parts each → 20 in-flight PUTs total
});Every part gets the same automatic retry with backoff,
so a single flaky part does not fail the whole file. A fired signal cancels all
in-flight parts and pending retries cleanly.
CORS
For multipart to work from the browser, the bucket's CORS config must expose the
ETag response header to the page's origin — the uploader reads each part's
ETag to complete the upload. If ETag is not exposed, the engine throws
NO_ETAG.
A working CORS rule allows PUT from your origin and exposes ETag:
[
{
"AllowedOrigins": ["https://your-app.example.com"],
"AllowedMethods": ["PUT", "GET"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 86400
}
]Single uploads need CORS too
The same PUT-from-your-origin rule is what lets the browser upload directly to the bucket at all
— multipart just additionally needs ETag exposed. Configure CORS once on the bucket and both
modes work.
Single-file size limit
Multipart raises the practical ceiling on file size, but the plan's
maxFileBytes still applies — createUpload rejects a file over it with
FILE_TOO_LARGE (413) before any URL is minted. See
Quotas & plan limits.
Abandoned uploads
A reserved-but-never-completed multipart upload leaves a pending row. A
scheduled job sweeps pending rows older than an hour, deleting any orphaned
bucket object and marking the row deleted — so an interrupted large upload never
lingers or counts against your quota.