What are chunked uploads?
A chunked upload splits one large file into many small HTTP requests instead of a single enormous one. It is the difference between "the 4 GB upload failed at 97%" and "the 4 GB upload retried one 5 MB part and carried on".
Why single-request uploads break
A plain multipart/form-data POST of a large file is one long-lived request. That runs into four walls:
- Request size limits. IIS and Kestrel cap request bodies (
maxAllowedContentLength,MaxRequestBodySize). Raising them globally to accept 4 GB weakens every other endpoint. - Timeouts. Proxies, load balancers and gateways cut long requests, often at 60–300 seconds.
- All-or-nothing failure. A dropped connection at 97% means starting from 0%.
- Memory pressure. Naive handlers buffer the whole body before writing it to disk.
How chunking fixes it
The browser slices the file with Blob.slice() and sends each part as its own request. The server writes each part to temporary storage and, once all parts have arrived, concatenates them into the final file.
POST /api/upload/chunk X-Chunk-Index: 0 X-Chunk-Count: 800
POST /api/upload/chunk X-Chunk-Index: 1 X-Chunk-Count: 800
...
POST /api/upload/chunk/complete { uploadId, fileName, totalChunks } Each request is small and short-lived, so it fits inside every limit above. A failed part costs one part, not the whole file.
Choosing a chunk size
There is no universal answer, but the trade-off is simple:
| Chunk size | Effect | Suits |
|---|---|---|
| Small (1–2 MB) | More requests and overhead, but cheap retries and smooth progress. | Mobile and unreliable networks. |
| Medium (5–10 MB) | A balanced default. | Most applications. |
| Large (25 MB+) | Less overhead, but an expensive retry and coarse progress. | Fast, stable LANs. |
Note: S3 multipart requires parts of at least 5 MB (except the last), and GCS resumable wants chunks that are a multiple of 256 KiB. CoreUpload aligns these for you.
The parts people get wrong
Chunking looks simple until it meets reality. These are the failure modes worth knowing about:
- "Sent" is not "stored". A part whose bytes left the browser may still have failed server-side. If you mark it complete on progress rather than on a 2xx response, resume can skip a part that was never written — and the assembled file is silently corrupt.
- Changing the chunk size between sessions. Resume state records part boundaries. If the size changes, old indexes map to different byte ranges. The safe move is to keep the original size for that upload, or discard the state.
- Trusting client state on resume. Temporary parts get cleaned up. Before resuming, ask the server which parts it still holds — do not assume.
- Retrying everything. A 413 or 401 will fail identically forever; retrying only burns time. Retry network errors and 5xx; honour
Retry-Afteron 429/503.
CoreUpload handles all four: parts are only recorded on a confirmed response, the persisted chunk size wins on resume, a chunk/status probe reconciles with the server, and retries are classified by status code.
Chunked uploads in ASP.NET Core
With CoreUpload the client and the receiving endpoints come from the same package:
// Program.cs
app.MapCoreUploadEndpoints(); <core-upload asp-upload-url="/api/upload/upload"
asp-chunked="true"
asp-chunk-size="5MB"
asp-chunk-concurrency="3"
asp-progress="true"></core-upload>