Large Files — Stream, Split or Sign
A Hoplink user tries to share a 2 GB conference video, and the upload fails. Nothing is wrong with R2: an object can be far bigger than that. The problem is the path the bytes take. A request passing through a Worker has a body size limit (100 MB on the Free and Pro plans), and a Worker has only 128 MB of memory. Big files need a different route.
The rule
Stream what fits; split what is big; sign what bypasses the Worker.
Three techniques, in order of file size:
1. Stream. Never load a file into memory with await request.arrayBuffer(). Pass the stream straight through: env.FILES.put(key, request.body) for uploads and new Response(object.body) for downloads. Memory stays small no matter the file.
2. Split (multipart upload). Break the file into parts, upload each separately, then stitch them together:
const upload = await env.FILES.createMultipartUpload("talks/keynote.mp4");
// each part arrives in its own request; every part but the last must be at least 5 MiB
const part1 = await upload.uploadPart(1, firstChunk);
const part2 = await upload.uploadPart(2, secondChunk);
await upload.complete([part1, part2]);Parts can upload in parallel, and a failed part can be retried alone instead of restarting the whole file. upload.abort() cancels it. The browser keeps track of the uploadId and part numbers between requests; the Worker resumes with env.FILES.resumeMultipartUpload(key, uploadId).
3. Sign (presigned URLs). Because R2 speaks the S3 API, your Worker can create a short-lived signed URL with an S3 client library and the bucket's access keys. The browser then uploads directly to R2, so the Worker's body limit never applies. The Worker only decides who may upload what, and for how long.
Downloads of large files benefit from range requests: env.FILES.get(key, { range: { offset, length } }) returns just one slice, which lets video players seek without fetching the whole file.
The shortcut
Decide by size, as a ladder:
Small (a profile photo, 2 MB)
Stream
request.bodyintoenv.FILES.put. Done in one request.
Trap: abandoned multipart uploads. Parts that were uploaded but never completed still take up storage until the upload is aborted or cleaned up — the subject of the next lesson.
Drills
Answer fast — trust the rule.
Why is const data = await request.arrayBuffer() a bad idea for a 90 MB upload?
In a multipart upload, what is the minimum size of every part except the last?
What is the main benefit of a presigned upload URL?
A video player wants to jump to minute 40 of a large file. Which read helps?
Remember
- Stream what fits; split what is big; sign what bypasses the Worker.
- Shortcut: small → stream, big → multipart, huge → presigned URL.
- Trap: loading whole files into memory, and leaving multipart uploads unfinished.
