Skip to content

Client side of large uploads

A large upload is numbered parts and their ETags: the client resends what lacks one, completes in order, and the API never carries the bytes.

Predict first

Before you read: predict

ExamHall's proctoring client uploads a candidate's 96 MiB webcam clip straight to storage as a multipart upload: 12 parts of 8 MiB, 4 at a time, each through its own presigned part URL. On the train home the connection drops. By then parts 1–8 have come back with their ETags, and parts 9–12 were each half sent. A minute later the connection is back.

Which recovery puts the whole clip in storage, sending the least after the reconnect?

Pick one

The 30-second version

The browser sends the bytes straight to storage; the API only authorises and records. Cut the file into numbered parts of at least 5 MiB, a few in flight at once. After a drop, keep the upload and send only the parts without an ETag, each from its first byte. Count progress by ETags, complete in order, cancel cleanly.

A large upload is numbered parts and their ETags: the client resends what lacks one, completes in order, and the API never carries the bytes.

How it works

Step 1 of 11

The bytes skip the API. The API starts a multipart upload and returns a presigned URL per part; the browser sends each part to storage.
In the kitchen
  • the diocese issues delivery passes and records the finished window; no panel passes through its office → the API (steps 1, 2)

Analogy: A stained-glass studio returning a restored cathedral window

At the studio In the upload
the window travels as numbered panels, several vans on the road at once; a panel's number is its place in the window numbered parts, sent in parallel (steps 3, 5)
the clerk of works signs a receipt for each panel, and the studio keeps every receipt ETags kept (step 6)
after a road closure, only panels without a receipt go out; one that was half-way travels again whole all or nothing, then resume (steps 7, 8)
the studio's chart counts signed receipts, not miles driven honest progress (step 9)
the glaziers set the window from the full receipt list, in number order Complete in order (step 10)
a called-off commission leaves panels in the cathedral store, charged until the studio has them cleared, once the vans on the road arrive cancel (step 11)
the diocese issues delivery passes and records the finished window; no panel passes through its office the API (steps 1, 2)

In real terms: storage keeps the parts; the client keeps the list of what landed and sends only what did not.

Breaks down when: a glass panel exists once and can shatter; a part's bytes can be read from the file again as often as needed, so a lost part costs only the time to resend it.

Contrasting case: a small file sent with one presigned PUT: no parts, so a drop sends it all again. AWS suggests multipart from about 100 MB.

Examples, run for real

4.1 ExamHall EXH-3430 → MR !3730: the clip through the API, then straight to storage

ExamHall's proctoring client sent each 200 MB recording to the NestJS API, which wrote it to storage in parts (EXH-3430). MR !3730 has the API start the multipart upload and hand out part URLs; the browser sends the parts to storage, 4 at a time, and the API completes the upload.

4.1 EXH-3430, through the API:     requests to the API 1 · file bytes the API read 200,000,000 and wrote to storage 200,000,000 · parts stored while that one request stayed open 24
4.1 MR !3730, straight to storage: requests to the API 2 · bytes the API read 1,598 (JSON only) · part PUTs from the browser to storage 24, 4 at a time · clip in storage byte for byte: yes

The file's bytes no longer cross the API (steps 1, 2): it answers two small calls and completes the upload.

4.2 Proof domain, chat: a voice-message video resumes after a drop

A chat app sends a 64 MiB video as 8 parts of 8 MiB, 2 at a time. The connection drops with parts 5 and 6 half sent.

4.2 drop: parts with an ETag 1, 2, 3, 4 · cut off on the way: part 5 (4 of 8 MiB sent), part 6 (4 of 8 MiB sent) · ListParts: 1, 2, 3, 4
4.2 reconnect: parts sent 5, 6, 7, 8, each whole, 32 MiB · Complete with parts 1–8 in order: 200 · video in storage byte for byte: yes

Storage holds only parts with an ETag (step 7); the app sends the rest whole and completes in order (steps 8, 10).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

ExamHall's design so far: the proctoring client records the webcam with MediaRecorder and start(10000), keeps the blobs, and uploads the clip in 8 MiB parts when the exam ends. The interviewer's next follow-up: "A laptop dies 4 minutes 55 seconds into a 6-minute practice exam. At most a minute of video may be missing from storage when that happens, and a normal exam must still end as one clip." At the bitrate used, each blob is 3 MiB.

Which change answers the follow-up?

Pick one

Question 2 of 7

A chat app uploads a 96 MiB video as 12 parts of 8 MiB, 4 at a time, and shows a progress bar. The connection drops when parts 1–4 have their ETags and parts 5–8 are each half sent; when it is back, the app sends parts 5–12 and completes.

Which way of counting keeps the bar from ever showing more of the video than storage holds, and ends at 100%?

Pick one

Question 3 of 7

A chat app uploads a 96 MiB video as 12 parts of 8 MiB, 4 at a time, with this code:

In every group of four, the third part's reply arrives first, then the first, second and fourth. Every PUT succeeds.

What does Complete do?

Code for this question
const done = [];await pool(parts, 4, async (part) => {  const res = await fetch(part.url, { method: 'PUT', body: part.blob });  done.push({ PartNumber: part.number, ETag: res.headers.get('ETag') });});await completeUpload(uploadId, done);
Pick one

Question 4 of 7

A chat app uploads a 96 MiB video as 12 parts of 8 MiB, 4 at a time. The sender taps Cancel when parts 1–4 have their ETags and parts 5–8 are each half sent. The bucket has only its default settings.

Which cancel is sure to leave none of the video's bytes stored a month later?

Pick one

Question 5 of 7

The interviewer says: "Candidates upload a 200 MB recording at the end of the exam. Design the upload." In 15 minutes, sketch your board: who carries the bytes and who records the clip, the part size and how many parts run at once, what the client keeps and what it does after a drop, what the progress bar counts, how Complete lists the parts, and what Cancel does.

Then write your choices in the shape of design.template.mjs as my-design.mjs, and run the check.

Question 6 of 7

ExamHall's proctoring client uploads a 200 MB recording as 24 parts of 8 MiB (the last one smaller), 4 at a time. The connection drops once: parts 1–16 have their ETags, and parts 17–20 are each half sent. When it is back, the client runs this code, where parts holds all 24 parts, each with its part URL and its whole Blob:

How many PUT requests does this code send?

Code for this question
const listed = await listParts(uploadId);const have = new Set(listed.map((p) => p.PartNumber));const missing = parts.filter((p) => !have.has(p.number));await pool(missing, 4, (p) => fetch(p.url, { method: 'PUT', body: p.blob }));
Pick one

Question 7 of 7

A chat app uploads a 96 MiB video as 12 parts of 8 MiB, 4 at a time. Part 5's PUT fails once with a 500, and the app retries it under the next unused number:

Every other PUT succeeds the first time. The app then calls Complete with all 12 parts' numbers and ETags, in ascending order.

What happens?

Code for this question
let nextNumber = 13;function retry(part) {  part.number = nextNumber++;  return put(part);}
Pick one

Go deeper

Pro tip
  1. Answer “why not through the API?” with what the API would carry: every byte in and out, and one request held open for the whole upload. When: the interviewer says presigned URLs are a risk and the API should validate the file. Status: SELF; not run.
Source

Valid as of 2026-10-09. Every rule of the store model is quoted from these pages in lib/store.mjs.

  • AWS, Uploading objects with presigned URLs (named in the curriculum row): https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html · aws-presigned-upload.txt. "A presigned URL is limited by the permissions of the user who creates it." Supports step 1 (signing itself belongs to api-file-uploads-presigned).
  • AWS, Uploading and copying objects using multipart upload (named in the curriculum row): https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html · aws-mpu-overview.txt. "You can upload these object parts independently, and in any order."; "If transmission of any part fails, you can retransmit that part without affecting other parts."; "you only need to retry uploading the parts that are interrupted during the upload"; "if you upload a new part using the same part number as a previously uploaded part, the previously uploaded part gets overwritten"; "For each part upload, you must record the part number and the ETag value."; "After you initiate a multipart upload, there is no expiry; you must explicitly complete or stop the multipart upload."; "To make sure you free all storage consumed by all parts, you must stop a multipart upload only after all part uploads have completed."; "we recommend that you configure a lifecycle rule to delete incomplete multipart uploads"; "It's a best practice to use multipart upload for objects that are 100 MB or larger"; "The list parts operation returns the parts information that you uploaded for a specific multipart upload."; "Note that the returned list of parts doesn't include parts that haven't finished uploading."
  • MDN, MediaRecorder (named in the curriculum row): https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder · mdn-mediarecorder.txt. "If this is specified, the media will be captured in separate chunks of that duration, rather than the default behavior of recording the media in a single large chunk." Supports step 4.
  • MDN, MediaRecorder: start() and dataavailable event: https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start · mdn-mediarecorder-start.txt; https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/dataavailable_event · mdn-dataavailable.txt. "Like other time values in web APIs, timeslice is not exact"; "a dataavailable event is fired every timeslice milliseconds". Supports step 4 (a blob about every 10 s).
  • AWS API Reference, UploadPart: https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart.html · aws-api-uploadpart.txt. "Part numbers can be any number from 1 to 10,000, inclusive. A part number uniquely identifies a part and also defines its position within the object being created." Supports steps 3, 7.
  • AWS API Reference, CompleteMultipartUpload: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CompleteMultipartUpload.html · aws-api-complete.txt. "The CompleteMultipartUpload API operation concatenates the parts that you provide in the list."; "The list of parts was not in ascending order. The parts list must be specified in order by part number."; "Each part must be at least 5 MB in size, except the last part." Supports steps 3, 10.
  • AWS API Reference, AbortMultipartUpload: https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html · aws-api-abort.txt. "if any part uploads are currently in progress, those part uploads might or might not succeed". Supports step 11.
  • AWS, Amazon S3 multipart upload limits: https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html · aws-mpu-limits.txt. "5 MiB to 5 GiB. There is no minimum size limit on the last part of your multipart upload." Supports step 3.

Checked against the tools and docs as of 2026-10-09.