Uploads and delivery
Design browser and multipart uploads
Choose Worker-proxied, presigned, or multipart upload flows based on file size, trust, and recovery needs.
8 minute lesson
There are three upload flows, and file size picks between them.
Small trusted uploads can pass through a Worker that authenticates, validates metadata, and streams to R2. One request, one put, done. This is the right answer for avatars and documents.
Direct browser uploads use a presigned URL: your server signs a short-lived URL for one specific key, and the browser sends the file straight to R2. The Worker never touches the bytes, which saves transfer, but you now need scoped authorization on the signing endpoint and correct CORS on the bucket so the browser’s PUT is allowed from your origin.
For large files, use multipart upload. It divides a large object into independently uploaded parts, then completes them as one object:
const upload = await env.FILES.createMultipartUpload('videos/launch-demo.mp4')
// each part arrives in its own request, resumed by uploadId
const resumed = env.FILES.resumeMultipartUpload(
'videos/launch-demo.mp4',
upload.uploadId
)
const part = await resumed.uploadPart(partNumber, request.body)
// part = { partNumber: 1, etag: "..." }
The client tracks each returned etag, and completion assembles the object:
await resumed.complete([
{ partNumber: 1, etag: 'bce6bf66aeb76c7040fdd5f4eccb78e6' },
{ partNumber: 2, etag: '8165449fc15bbf43d3b674595cbcc406' },
])
The rules on parts are strict. Every part except the last must be at least 5 MiB, all parts except the last must be the same size, and one object can have at most 10,000 parts. Get the sizing wrong and complete rejects the upload.
The win is resumability. A failed part retries alone. A dropped connection resumes from the last confirmed part number instead of starting a 2 GB transfer over.
The cleanup nobody does
Abandoned multipart uploads keep their parts, and you pay for that storage even though no object ever appears. Call abortMultipartUpload when a user cancels. R2 also aborts incomplete multipart uploads automatically after seven days by default, so abandoned parts don’t linger forever, but explicit cleanup is still cheaper.
One more rule that applies to all three flows: never trust a browser-provided content type alone. The client said image/png; that’s a claim, not a fact. Validate it server-side before you store it as metadata.
Draw both a small avatar upload and a resumable large-video upload, including the server authorization step. If your diagram has no arrow labeled “server checks who is asking”, the design has a hole.
Lesson completed