Moving large files reliably
Open in the interactive app: practice, quiz, mock interview →In one minute
- Large files (videos, backups, datasets, design files) should never flow through your application servers. Clients upload directly to object storage using short-lived pre-signed URLs, and downloads come from object storage or a CDN.
- Big uploads are split into chunks (parts) that upload in parallel, retry independently and resume after a network drop. The server assembles them only when all parts are present and verified.
- Your database stores metadata (owner, size, checksum, status, storage key), never the bytes. Object storage stores bytes.
- Downloads use HTTP range requests, so clients can resume, seek and fetch in parallel.
- In interviews, the pattern shows up in Dropbox, Google Drive, YouTube upload, photo sharing, file attachments in chat and any "users upload X" requirement. The probes are about resumability, integrity, the upload state machine and abuse.
How it works
Why not through the app server
An app server that proxies a 4 GB upload holds a connection and buffers for minutes, ties up a worker or thread, doubles network traffic (client to app, app to storage), and loses everything if it is redeployed mid-upload. Object stores are built for this traffic; your servers are not. So the app server's job shrinks to authorization and bookkeeping.
The upload flow
- The client calls
POST /uploadswith file name, size and content type. The server checks quota and permissions, creates a metadata row with statusPENDING, and starts a multipart upload in object storage, receiving an upload ID. - The server returns pre-signed URLs, one per part (or issues them in batches on request). Each URL authorizes a
PUTof one part until it expires, and it can be replayed any number of times before then. S3 checks expiry when a request starts, and a URL signed with temporary role credentials dies when that session does, by default after an hour (AWS docs), so clients must be able to ask for fresh URLs mid-upload. - The client uploads parts in parallel, typically 3 to 8 at a time, each 8 to 64 MB. It records which parts succeeded and their ETags or checksums.
- On a network drop, the client asks the server (or the store) which parts exist and uploads only the missing ones.
- The client calls
POST /uploads/{id}/completewith the part list. The server completes the multipart upload, verifies size and checksum, and moves the row toUPLOADED. - An event on completion triggers post-processing: virus scanning, thumbnailing, transcoding, indexing. The row moves to
READYwhen done.
Chunking for sync and deduplication
Sync products go further: they split files into content-defined or fixed-size blocks and name each block by its hash; Dropbox's storage keys blocks of up to 4 MB by their SHA-256 (Dropbox Tech). Uploading a file becomes "which of these block hashes do you not already have?", so editing one page of a large document uploads one block, and two users uploading the same file store it once. The file's metadata becomes an ordered list of block hashes.
Serving downloads
For public or widely shared files, put a CDN in front of the store. For private files, the server checks permissions and returns a pre-signed GET URL or a signed CDN URL with a short expiry. Clients use Range headers to resume broken downloads and to fetch parts in parallel; the store answers 206 Partial Content, and an If-Range validator makes sure the bytes still belong to the same version (RFC 9110).
How it shows up in interviews
Triggers: design Dropbox or Google Drive, design YouTube or Instagram upload, design an attachment service for email or chat, design a backup service. A strong answer keeps bytes off app servers, uses pre-signed multipart uploads with resumability, defines the metadata state machine, verifies integrity end to end, and handles post-processing asynchronously. Interviewers probe: how a client resumes after a laptop sleeps, how you stop someone uploading 1 TB against a 10 GB quota, what happens to abandoned uploads, how you deduplicate, and how a shared link to a private file stays secure.
Down the stack
Layer 1The contract
A pre-signed part URL promises that exactly that operation (method, bucket, key, part number, sometimes content length and checksum) is allowed until expiry; anyone holding the URL can use it, so it is a bearer token. Multipart upload promises atomic visibility: the object does not exist until complete succeeds, and then it appears whole. Parts can be re-uploaded; the last successful upload of a part number wins. Your API promises that a file in READY has the declared size and checksum and has passed scanning. Range requests promise byte-exact slices of an immutable object, which is why objects should be immutable and new versions get new keys.
Layer 2The mechanism
In Amazon S3, CreateMultipartUpload returns an UploadId; each UploadPart takes a PartNumber from 1 to 10,000 and a body of at least 5 MiB (except the last) and at most 5 GiB, and returns an ETag; CompleteMultipartUpload takes the ordered list of part numbers and ETags. The object size limit is now 48.8 TiB, not the 5 TB that older material quotes, and ListParts returns at most 1,000 parts per page, so a resuming client must paginate (AWS docs). The resulting ETag of a multipart object is the MD5 of the concatenated part MD5s plus -N, not the MD5 of the file, so use explicit checksums. S3 now attaches a CRC64NVME checksum by default when the client sends none, and CRC-based algorithms support a "full object" checksum on multipart uploads, so you can check one whole-file value without tracking part boundaries; SHA-256 works only as a composite of part checksums (AWS docs). Incomplete uploads keep their parts, and their storage charges, until aborted, which is what the AbortIncompleteMultipartUpload lifecycle rule is for. Pre-signed URLs carry a SigV4 signature in the query string (X-Amz-Signature, X-Amz-Expires, X-Amz-Credential). Google Cloud Storage uses a resumable session URI instead, valid for a week, where the client sends chunks in multiples of 256 KiB with Content-Range and asks for the committed offset after a failure; Google suggests one chunk when possible, since each extra request adds latency and cost (Google Cloud docs). The open tus protocol standardises the same idea: HEAD returns Upload-Offset, and PATCH sends bytes starting at that offset. Its successor is an IETF working-group draft, still not an RFC at version 13 in October 2026, which adds an Upload-Complete header and a 104 interim response that hands the client a resume URL before the body is sent (IETF draft).
Layer 3The machine
Upload time is bound by the client's uplink. A 2 GB file over a 20 Mbit/s home uplink takes about 800 seconds even with perfect parallelism, so resumability matters more than speed. A single TCP connection's throughput is limited by its congestion window over RTT; on a 100 ms path, one connection with a 1 MB window caps near 80 Mbit/s, and one lost packet halves the window under classic congestion control, which is why parallel parts over several connections fill a fat pipe better. On the serving side, a well-built file server avoids copying bytes through user space: sendfile(2) moves data from the page cache to the socket inside the kernel (man7), and kernel TLS (kTLS) lets that work for HTTPS too. Storage servers erasure-code objects across disks and racks (for example splitting data into k data and m parity fragments), so a whole-object read touches many disks in parallel, and a 1 KB metadata row lives in a database built for small random reads.
Why it's built this way
Bytes and metadata have opposite access patterns (large sequential versus small random), so they live in different systems. Client networks are slow and flaky, so uploads must resume from parts instead of restarting. App servers are expensive per connection and redeployed often, so they hand clients a signed capability and step aside. Objects are immutable once complete, which makes caching, ranges and CDN distribution safe.
Diagram
sequenceDiagram
participant C as Client
participant A as Upload API
participant DB as Metadata DB
participant S as Object store
C->>A: POST /uploads (name, size)
A->>DB: insert PENDING row
A->>S: CreateMultipartUpload
A-->>C: upload id and pre-signed part URLs
par parallel parts
C->>S: PUT part 1
C->>S: PUT part 2
end
C->>A: complete (part list, checksum)
A->>S: CompleteMultipartUpload
A->>DB: status UPLOADED
S-->>A: event: scan and processWorked example
A design-file sharing product has 5 million uploads per day with a median size of 40 MB and a p99 of 3 GB. Total ingest is about 200 TB per day.
If uploads went through app servers, average ingest of 200 TB per day is about 18 Gbit/s sustained, with daytime peaks of several times that, all entering and leaving app servers. With direct-to-store uploads, the API handles about 120 create and complete calls per second on average, each a few kilobytes.
Part size is 16 MB. A 3 GB file is 192 parts; with 6 in parallel over a 50 Mbit/s uplink it takes around 8 minutes. If Wi-Fi drops at minute 6, the client lists parts already uploaded (roughly 140) and resends only the rest, losing at most the 6 parts in flight, about 96 MB. A lifecycle rule aborts multipart uploads that are not completed within 7 days; before it existed, the team found tens of terabytes of orphaned parts quietly billed each month.
Integrity: the client computes a SHA-256 per part and for the whole file while reading it. The store verifies part checksums on receipt, and the server checks the whole-file checksum after completion before marking the file UPLOADED.
In the wild
- Microsoft, 2023: a storage token far broader than intended. A researcher shared AI training data on GitHub through an Azure blob URL whose SAS token opened the whole storage account, including workstation backups and Teams messages of two former employees. Wiz reported it on 22 June and the token was revoked two days later. Microsoft extended secret scanning to flag SAS tokens with long expiry or broad permissions and now recommends lifetimes of an hour or less (MSRC).
- GitLab, 2021: tens of thousands of dangling multipart uploads. A self-managed GitLab customer found more than 67,276 incomplete S3 multipart uploads created in three days: the temporary files were cleaned up, but the uploads were never completed or aborted, so their parts kept billing. The advised fix was an S3 lifecycle rule that aborts incomplete uploads (GitLab issue).
- AWS, 2017: the object store itself goes down. A mistyped command removed too much capacity from S3's index and placement subsystems in us-east-1, and both needed a full restart that had not been done at that scale in years. S3 there was unavailable for about four hours, taking EC2 launches, Lambda and AWS's own status page with it. Any upload design that assumes one region's store is always up inherits this risk (AWS).
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Proxy through app servers | Simple, full control of bytes | Costly, fragile, double bandwidth | Small files only (a few MB) |
| Single pre-signed PUT | Simple, no app bandwidth | No resume, size limits per request | Files up to tens of MB |
| Multipart with pre-signed parts | Parallel, resumable, very large files | More client logic, orphan cleanup | Large uploads, unreliable networks |
| Content-addressed blocks | Dedup and delta sync | Hash index, garbage collection of blocks | Sync and backup products |
Pitfalls in production
- Orphaned parts. Abandoned multipart uploads accumulate silently and cost money. Add an abort-incomplete lifecycle rule and a sweeper for
PENDINGrows. - Trusting declared size and type. Clients lie. Enforce size limits in the signed request where possible, check the final size on completion, and sniff content type server-side.
- Long-lived pre-signed URLs. They are bearer tokens and leak through logs and browser history. Keep expiries to minutes and scope each URL to one key; Azure's valet-key example issues a three-minute, create-only token for one blob, which also makes it effectively single-use (Azure Architecture Center).
- Serving before scanning. If the file is downloadable the moment upload completes, malware spreads. Gate on a
READYstate after scanning. - Mutable objects behind a CDN. Overwriting a key leaves stale copies at the edge and breaks range resumes. Write new versions to new keys, and enforce it: a pre-signed
PUTsilently replaces an existing key, butIf-None-Match: *onPutObjectorCompleteMultipartUploadmakes S3 refuse with412(AWS docs). - Metadata and bytes drifting apart. A crash between the store and the DB leaves rows without objects or objects without rows. Reconcile with a periodic job keyed on upload ID.
Leadership lens
- Make the upload state machine explicit and observable: counts per state, time stuck in each state, and failure reasons.
- Storage and egress are usually the biggest cost lines. Ask about lifecycle tiers, dedup ratios and CDN hit rate early.
- Decide the client SDK strategy. Resumable uploads live mostly in client code, so mobile and web teams need a shared library and tests against flaky networks.
- Plan for abuse: per-user quotas, rate limits on upload creation and takedown workflows.
Check yourself
Why should part uploads be independent and retryable?
Show answer
Networks drop mid-transfer, and independent parts mean a failure costs one part rather than the whole file.
Why not compare the multipart ETag with an MD5 of the file?
Show answer
The multipart ETag is derived from the parts, not the whole file's MD5, so integrity needs an explicit checksum computed the same way on both sides.
What does content-addressed chunking enable?
Show answer
Uploading only blocks the server lacks, so edits send small deltas and identical content is stored once.
Why do completed objects get new keys instead of being overwritten?
Show answer
Immutable keys make CDN caching and range resumes safe, since a cached or partially downloaded object can never change underneath.
Sources and further study
Primary
- IETF draft: Resumable Uploads for HTTP: the standards-track successor to tus, with upload resources and offsets.
- RFC 9110, section 14: Range Requests:
Range,If-Range,206and multipart byte ranges.
Systems docs
- Amazon S3: multipart upload limits: object size, part count and part size limits.
- Amazon S3: checking object integrity for uploads: full-object versus composite checksums and how the multipart ETag is built.
- Amazon S3: conditional writes:
If-None-MatchandIf-Matchon puts and multipart completion.
Books
- System Design Interview Vol. 1, Alex Xu, chapter 15 "Design Google Drive": block servers, delta sync, resumable upload and metadata.
- System Design Interview Vol. 2, Alex Xu and Sahn Lam, chapter 9 "S3-like Object Storage": data and metadata stores, multipart upload and erasure coding.
- Understanding Distributed Systems (2nd ed.), Roberto Vitillo, chapter "File storage": how a blob store such as Azure Storage separates its layers.
Other curricula
- Azure Architecture Center: Claim-Check pattern: frames large payloads as something you store once and pass by reference, keeping them out of messaging and app paths.
Watch
- Dropbox Background on Magic Pocket with James Cowling (Tech Field Day): how Dropbox stores file blocks at scale after leaving S3.
- Dive deep on Amazon S3 (AWS re:Invent 2024): how a large object store is built and what that means for clients.
Free resources
- Uploading and copying objects using multipart upload (AWS docs): part limits, completion and abort semantics.
- Uploading objects with presigned URLs (AWS docs): how signed URLs delegate a single upload.
- Resumable uploads (Google Cloud Storage): session URIs, chunk sizes and resuming after failure.
- tus resumable upload protocol: an open HTTP protocol for resumable uploads.
- Inside the Magic Pocket (Dropbox Tech): block storage, replication and verification for user files.
- Valet Key pattern (Azure Architecture Center): delegating direct storage access with scoped tokens.