Multipart Uploads
Split large files into parts, upload them in parallel, and finalize when every part lands.
When to use this
- Files larger than your app's
maxFileSize(single PUT limit). - Long uploads on flaky networks that need per-part resumability.
- When you want parallelism — multiple parts in flight at once.
Server-side only for v1
1. Init
Reserve a multipart upload. You'll get back an uploadId, key, and fileId that thread through the rest of the flow.
const init = await storage.initMultipart({
name: 'movie.mp4',
size: totalBytes,
contentType: 'video/mp4',
metadata: { userId: 'abc' },
})2. Presign + PUT each part
Slice the file into chunks, presign one URL per part, PUT the bytes, and collect each part's ETag header.
const PART_SIZE = 5 * 1024 * 1024 // 5 MB minimum
const numParts = Math.ceil(totalBytes / PART_SIZE)
const parts: Array<{ partNumber: number; etag: string }> = []
for (let i = 0; i < numParts; i++) {
const partNumber = i + 1
const chunk = file.slice(i * PART_SIZE, (i + 1) * PART_SIZE)
const { uploadUrl } = await storage.presignMultipartPart(
init.uploadId,
init.key,
partNumber,
)
const res = await fetch(uploadUrl, { method: 'PUT', body: chunk })
const etag = res.headers.get('etag')!.replaceAll('"', '')
parts.push({ partNumber, etag })
}Part size & count
3. Complete
Hand back the ordered list of { partNumber, etag } pairs. Parts are stitched in part-number order — your loop order doesn't matter.
const file = await storage.completeMultipart({
uploadId: init.uploadId,
key: init.key,
fileId: init.fileId,
parts,
})
console.log(file.id, file.url)4. Abort on failure
If anything throws — bad chunk, network blip, user cancel — abort the upload to free the slot immediately.
try {
// ...init, presign loop, complete...
} catch (err) {
await storage.abortMultipart(init.uploadId, init.key, init.fileId)
throw err
}Parallelism
Parts are independent — fire 3–5 in parallel and you'll usually saturate the upload pipe. Watch for memory: buffered chunks live in your process until the PUT settles.
import pLimit from 'p-limit'
const limit = pLimit(4) // 4 parts in flight
const parts = await Promise.all(
Array.from({ length: numParts }, (_, i) => limit(async () => {
const partNumber = i + 1
const { uploadUrl } = await storage.presignMultipartPart(init.uploadId, init.key, partNumber)
const res = await fetch(uploadUrl, { method: 'PUT', body: file.slice(i * PART_SIZE, (i + 1) * PART_SIZE) })
return { partNumber, etag: res.headers.get('etag')!.replaceAll('"', '') }
})),
)