Tanstack Start

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 sizeUpload modeParts
Under 100 MBsingleOne presigned PUT.
100 MB and upmultipartOne presigned PUT per 16 MB part.

How it flows

  1. createUpload initiates a multipart upload on the bucket and returns a presigned PUT URL for each part, plus the uploadId and partSize.
  2. The uploader sends parts concurrently — 6 at a time by default (partConcurrency) — straight to the bucket, slicing the file locally per the partSize.
  3. Each part returns an ETag. The uploader collects them and passes them to complete.
  4. 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.

On this page