SonusLab StorageSonusLab Storage

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

The browser multipart helper is planned for a future release. For now, run multipart from your backend — stream the file there first or proxy chunks through your server.

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

Parts must be at least 5 MB (except the last one) and a single upload is capped at 10,000 parts. 5–10 MB per part is a good default.

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('"', '') }
  })),
)