Resumable uploads explained
Chunking survives a failed request. Resumability survives a closed laptop. They are related but not the same thing, and the difference is where most upload bugs live.
Two different questions
A resumable upload has to answer both:
Only the server knows what it actually persisted. The client's belief is a cache, and caches go stale.
After a reload the browser loses its File handles. Without the bytes, "resume" means "ask the user to pick the file again".
How each protocol answers question 1
| Protocol | "Where was I?" | Notes |
|---|---|---|
| tus 1.0 | HEAD the upload URL, read Upload-Offset. | Purpose-built for this. A PATCH continues from that byte. |
| S3 multipart | ListParts on the upload id. | Parts persist server-side until you complete or abort the session. |
| GCS resumable | PUT with Content-Range: bytes */TOTAL → a 308 plus a Range header. | The session URL is the handle; it can expire. |
| Server chunked | Ask your own endpoint which parts it holds. | CoreUpload exposes GET /api/upload/chunk/status for exactly this. |
Answering question 2: keeping the bytes
Browsers do not let you re-open a file the user picked earlier — a File handle does not survive a reload. There are two options:
- Ask the user to re-select the file. Works everywhere; mildly annoying. The upload still resumes at the right offset.
- Persist the blob in IndexedDB. The file survives the reload and the upload continues with no interaction. This is what CoreUpload's
persistBlobsoption does (Uppy calls the equivalent Golden Retriever).
Storage note: blobs cost quota. Persisting a 2 GB file is a real 2 GB of browser storage, and a quota failure should be surfaced rather than swallowed — otherwise "your upload will resume" is a promise you silently broke.
The trap: trusting your own state
The tempting implementation is to persist "chunks 0–399 are done" and, on resume, skip to 400. That breaks when:
- A temp-file cleanup job removed the parts overnight.
- The S3 lifecycle rule aborted the multipart session.
- The tus session expired, or the GCS session URL is gone.
- The final chunk's response was lost, so the client never learned it landed.
In each case you assemble a file with a hole in it — and the corruption is silent. The fix is to treat client state as a hint and reconcile with the server before resuming. CoreUpload probes chunk/status (or ListParts / HEAD) and re-uploads anything the server cannot confirm.
Resumable uploads in ASP.NET Core
<core-upload asp-upload-url="/api/upload/upload"
asp-strategy="tus"
asp-persist-state="true"
asp-persist-adapter="indexeddb"
asp-persist-blobs="true"></core-upload> Reload the page mid-upload and the queue comes back with its progress intact.