System designCore10 min
Resumable & chunked uploads
A large upload over a home or mobile connection will be interrupted, and an upload that starts over after every drop may never finish. Resumable uploads let the server remember how many bytes it has, so the client continues from there; chunked and multipart uploads cap what a failure costs and let parts travel in parallel.
1 · The problem
A long upload over a flaky link fails late, and starting over makes it worse
A podcast hosting platform takes episodes from creators at home. Ade records a video episode of 4 GB and uploads it over a connection that sends 20 megabits a second, 2.5 MB a second. Without interruptions that takes 4,000,000,000 ÷ 2,500,000 = 1,600 seconds, about 27 minutes. But the connection drops now and then: the router restarts, the laptop sleeps, the phone hotspot switches cells.
Suppose drops arrive at random, on average once every 15 minutes. The chance of 27 minutes with no drop is e^(−1,600 ÷ 900) ≈ 17%. With a plain single-request upload, every drop throws away everything sent so far, and each retry faces the same odds. Making the upload resumable changes the shape of the curve:
- Restart after each drop
- Resume from last 8 MiB chunk
2.5 MB/s, drops at random on average every 15 minutes, reconnect time ignored. Restarting: expected time = (e^(λt) − 1) ÷ λ with λ = 1/900 s, so 4 GB takes about 74 minutes instead of 27. Resuming loses at most one chunk per drop and stays on the straight line.
Restarting grows exponentially with the transfer time: past some size, the expected time is hours and many attempts never finish. Resuming grows linearly, because a drop costs only the bytes since the last point the server confirmed. That's the whole idea; the rest of this page is the protocol, the sizing and the cleanup.
2 · The protocol
The server remembers how many bytes it has, and the client asks before continuing
A resumable upload first creates an upload resource on the server, with its own URL. The client sends bytes to it; after a failure, the client asks the upload how many bytes arrived, and sends the rest from that offset. The open tus protocol is the clearest example:
The client never assumes what arrived; it asks. A PATCH whose offset doesn't match the server's is refused with 409 Conflict, so bytes can't land in the wrong place.
The protocols in use differ in detail but share that shape:
Protocol | Start | Find the offset | Notes |
|---|---|---|---|
tus | POST creates the upload | HEAD returns Upload-Offset | Optional checksum and expiry extensions |
Google Cloud Storage | A session URI per upload | Each chunk gets 308 Resume Incomplete with a Range of what was stored | Don't assume all bytes sent arrived; finish within a week |
IETF draft for HTTP | The first request starts the upload | Upload-Offset field | Still a draft; Upload-Complete marks the last request |
The confirmed offset is a promise: it's a guarantee that those bytes won't need sending again, so the client can free them. A session URL is also a credential. Anyone holding the session URI can upload data to the bucket without further authentication, so treat it like the signed URLs in Object & blob storage.
3 · Chunks and parts
Chunks cap what a failure costs and allow parallel parts, at the price of more requests
A resumable upload can still send the file as one long request and resume wherever it broke. Splitting it into chunks or parts helps when requests have a time or size limit, when the total size isn't known at the start, or when parts should travel in parallel. In Cloud Storage a single chunk is usually best, since fewer requests means better performance; chunks, when used, must be multiples of 256 KiB, and at least 8 MiB is a sensible size.
S3's multipart upload goes further: the file is split into numbered parts that upload independently, in any order and in parallel, and a failed part is resent alone. When every part is in, a final request lists the parts and S3 assembles the object. On S3, multipart pays off from about 100 MB. The limits shape the part size:
S3 multipart limit | Value | Consequence |
|---|---|---|
Part size | 5 MiB to 5 GiB; the last part may be smaller | Small files don't need multipart |
Parts per upload | At most 10,000 | Part size ≥ file size ÷ 10,000 |
Ade's 4 GB file | 4,000,000,000 ÷ 10,000 = 400 KB minimum | Any part from 5 MiB works; 16 MiB gives 239 parts |
A 200 GB recording | 200,000,000,000 ÷ 10,000 = 20 MB minimum | Parts must be at least 20 MB |
Bigger parts mean fewer requests and less overhead; smaller parts mean less resent after a failure and less memory held per part. Parallel parts help when a single connection doesn't use the whole link, and gain nothing when the uplink itself is the limit: they just share it. The podcast app uploads up to three parts at a time and picks the part size from the file size.
4 · Upload state
Track each upload as a record with states, and keep the resume handle on the device
Big files should bypass the application servers: the app creates the upload and hands the client a short-lived URL that goes straight to storage (Object & blob storage). The application still needs to know where each upload stands, so it keeps a record of it, separate from the bytes:
The API never carries the 4 GB; it creates the upload, records it, and issues URLs. The browser sends the bytes straight to storage.
The record moves through clear states, each set by one actor:
- Initiated: the API created the upload, with the expected size and type.
- Uploading: parts or bytes are arriving; the confirmed offset or part list lives in storage, not in the client's memory.
- Uploaded: the client called complete, and storage assembled the object.
- Verified: the server checked size, checksum and real content type.
- Processed: transcoding and publishing are done (Background jobs & scheduling).
The resume handle, the upload id or session URL, must survive the browser tab. The podcast app stores it locally with the file's name, size and modification time, so that after a crash or a closed laptop it can offer "Resume upload of episode-42.mp4?" and check it's the same file before continuing (Offline-first & optimistic UI).
5 · Integrity
Check every part and the whole file, because resuming adds new ways to corrupt data
Resuming creates failure modes a single upload doesn't have: a chunk sent from the wrong position, a part from an older version of the file, a client that resumes after the user edited the file. Checksums catch them, at two levels:
- Per part: the client sends a checksum with each chunk or part, and the server rejects data that doesn't match. tus has an optional checksum extension for each PATCH; S3 accepts a checksum with each part.
- Whole file: a checksum of the complete file, checked after assembly, proves the parts add up to what the client has.
S3 offers two kinds for multipart uploads. A composite checksum is computed from the parts' checksums, so it depends on how the file was split. A full object checksum covers the whole file whatever the split, using CRC-based algorithms (CRC-64/NVME, CRC-32 or CRC-32C). AWS's SDKs calculate a checksum automatically when the caller doesn't supply one, and S3 fails a request whose data doesn't match it with a BadDigest error.
For the podcast app, the browser computes the full-file checksum as it reads the file, sends it with the completion request, and the upload only moves to verified if storage agrees.
A damaged part is caught on arrival and resent; the whole-file checksum then proves the parts add up to the file the browser read.
Then check the content itself. A file named .mp4 can be anything; the server reads its real type and, for media, probes that it decodes, before it reaches the transcoding queue. The size declared at the start bounds the upload, so a client can't keep appending past what it asked for.
6 · Cleanup
Abandoned uploads cost money until something deletes them
Many uploads are never finished: the user gives up, picks another file, or never comes back. Each one leaves stored bytes behind, and in S3 the parts of an incomplete multipart upload are billed until the upload is completed or stopped. They don't appear as objects in the bucket, so they are easy to forget.
System | What cleans up abandoned uploads |
|---|---|
S3 multipart | A lifecycle rule, AbortIncompleteMultipartUpload, after a set number of days since the upload began |
Google Cloud Storage | Resumable sessions must complete within a week |
tus servers | Optional expiration: the server may remove unfinished uploads after the time in Upload-Expires |
The uploads table | A periodic job marks old initiated or uploading rows as abandoned |
The window is a trade-off: long enough that Ade can resume after a weekend away, short enough that abandoned bytes don't pile up. Per-user limits on concurrent and total pending uploads stop one account from filling storage with uploads it never finishes.
7 · The checklist
Make large uploads resumable, verifiable and self-cleaning
For the podcast platform, and any product that accepts large files from users' own connections:
Decision | Podcast platform choice |
|---|---|
Protocol | A resumable protocol, tus or the storage's own resumable or multipart API, for files above about 100 MB |
Path | Straight to object storage with short-lived URLs; the API only creates and records |
Part size | At least file size ÷ 10,000; 16 MiB by default; up to 3 parts in parallel |
Resume | Ask the server for the offset or part list; never trust the client's memory |
Resume handle | Stored on the device with file name, size and modified time |
Integrity | Per-part checksums and a full-file checksum checked before verified |
Cleanup | Abort incomplete uploads after 7 days; per-user pending limits |
Check yourself
4 questions
Takeaways
Remember this
- Restarting a large upload after each drop makes expected time grow exponentially with size; resuming keeps it linear.
- A resumable upload is a server-side resource with a confirmed offset; after a failure the client asks for the offset and continues from there.
- Chunks and multipart parts cap the cost of a failure and allow parallelism; size parts within the store's limits, such as S3's 10,000 parts.
- Track each upload as a record with states, keep the resume handle on the device, and verify per-part and whole-file checksums before using the file.
- Abandoned uploads cost storage until a lifecycle rule or expiry removes them.