Making uploads fast
Two settings account for most upload throughput: how big each chunk is, and how many are in flight at once. Both have a sweet spot, and both get worse if you push them too far.
Why a single request is slow
One request means one TCP connection, and a single connection takes time to reach full speed — slow start, then congestion control reacting to every hiccup. On a long-distance link, latency alone caps a single stream well below the available bandwidth. Several connections in parallel recover that headroom, which is the real reason chunked uploads are usually faster and not just more reliable.
Choosing a chunk size
Every chunk carries fixed overhead: a request, headers, a server round-trip, a disk write. Small chunks mean more overhead per byte. Large chunks mean more to redo when one fails, and coarser progress.
| Situation | Chunk size | Reasoning |
|---|---|---|
| Mobile / unreliable networks | 256 KB – 1 MB | A failed chunk costs little to retry |
| General web (default) | 1 – 5 MB | Good overhead-to-retry balance |
| LAN / datacentre | 8 – 16 MB | Failures are rare; minimize round-trips |
| Direct-to-S3 multipart | 5 MB minimum | S3 requires ≥5 MB for all but the final part |
There is also a hard ceiling worth knowing: S3 allows at most 10,000 parts, so chunkSize × 10000 must exceed your largest file. At 5 MB that is a 50 GB limit.
Concurrency, and where it stops helping
Sending several chunks at once fills the pipe. But browsers cap connections per host (commonly six on HTTP/1.1), the user's uplink is finite, and your server pays for every concurrent write. Past a certain point additional parallelism just adds contention — and on a congested link it actively slows things down.
<core-upload asp-chunked="true"
asp-chunk-size="5242880" <!-- 5 MB -->
asp-chunk-concurrency="4" /> Three or four is a good default. Rather than guessing, you can let it tune itself — concurrency then rises while transfers succeed and backs off on errors, the same additive-increase/multiplicative-decrease idea TCP uses:
CoreUpload.create('#uploader', {
chunked: true,
chunkConcurrency: 'auto' // adapts to the observed network
}); Do the expensive work off the main thread
Hashing a gigabyte or re-encoding a 40-megapixel photo on the UI thread freezes the page — the upload may be fast while the experience is terrible. Both run in Web Workers here, so the interface stays responsive. If you enable client-side image resizing, remember the trade: a few hundred milliseconds of CPU can remove tens of megabytes from the transfer, which is almost always the better deal on a slow connection.
Get the first and last chunk in early
If your server validates file headers or trailers (magic bytes, an archive's central directory), uploading the first and last chunk before the middle lets it reject a bad file after a few hundred kilobytes instead of after several gigabytes:
<core-upload asp-chunked="true" asp-prioritize-first-last-chunk="true" /> Server-side costs that dominate
- Buffering instead of streaming. Reading a whole upload into memory before writing it caps your concurrency at RAM ÷ file size. Stream to disk and honor backpressure.
- Assembly I/O. Reassembling chunks reads and writes the entire file again. On a busy server that doubling is often the real bottleneck — direct-to-cloud avoids it entirely.
- Synchronous scanning. Antivirus and image processing inline with the request hold a connection open for seconds. Queue them and respond as soon as the bytes are safe on disk.
- Antivirus on the hot path. Scan asynchronously and gate access to the file until the verdict arrives, rather than making every uploader wait.
When to skip your server entirely
If files are large and you are not transforming them, direct-to-cloud is the biggest single win available: bytes go from the browser to S3, Azure Blob or GCS, and your server only signs requests. Your bandwidth bill, your CPU and your disk all stop being part of the upload path. See direct-to-cloud uploads — and read the security notes on signing endpoints before you ship it.
Measure, don't guess
Throughput depends on your users' networks, your hosting, and the file sizes you actually receive — none of which a table on a web page knows. Instrument real uploads (bytes, duration, retries, failures), then change one variable at a time. The concurrency demo lets you feel the difference immediately.