Upload 50GB Files from the Browser
Upload 50GB Files from the Browser
Uploading a 50GB file is very different from uploading a normal document or image. Sending the entire file through a single HTTP request creates problems with timeouts, network failures, memory usage, and retrying.
A better approach is to split the file into smaller chunks and upload them independently.
In this article, we'll look at a practical architecture using jQuery + ASP.NET Core + Azure Functions + Azure Blob Storage.
Why Not Upload the Entire File at Once?
Imagine uploading a 50GB file using a single request:
┌──────────────┐
│ Browser │
└──────┬───────┘
│
│ 50 GB
▼
┌──────────────┐
│ .NET API │
└──────┬───────┘
│
▼
┌──────────────┐
│ Azure │
│ Function │
└──────┬───────┘
│
▼
┌──────────────┐
│ Azure Blob │
│ Storage │
└──────────────┘
Now imagine the network connection fails after 45GB.
The upload may need to start again from the beginning.
50 GB
│
├─────────────────────────────────────┤
✗
Network fails
Restart
│
├─────────────────────────────────────┤
0 GB 50 GB
This is inefficient and frustrating for users.
Instead, we can divide the file into smaller pieces.
Chunked Upload
With chunked uploading, a 50GB file can be divided into 10MB chunks.
50 GB File
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Chunk 0 Chunk 1 Chunk 2
10 MB 10 MB 10 MB
│ │ │
└──────────────┼──────────────┘
│
...
│
▼
Chunk 5119
Approximately:
50 GB / 10 MB ≈ 5,120 chunks
Each chunk becomes an independent upload request.
Architecture
For an application where the browser communicates with an ASP.NET Core API, which then calls an Azure Function, the flow can look like this:
┌─────────────────────┐
│ Browser │
│ jQuery │
│ │
│ Split File into │
│ Chunks │
└──────────┬──────────┘
│
│ 10 MB Chunk
▼
┌─────────────────────┐
│ ASP.NET Core │
│ API / │
│ Gateway │
└──────────┬──────────┘
│
│ Stream Chunk
▼
┌─────────────────────┐
│ Azure Function │
│ │
│ Process / Store │
│ Chunk │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Azure Blob │
│ Storage │
└─────────────────────┘
The important point is that the browser performs the chunking.
The ASP.NET Core API should receive one chunk at a time and stream it forward rather than receiving the entire 50GB file.
Why Chunk in the Browser?
The browser already gives us access to the File API.
For example:
const chunk = file.slice(start, end);
This allows us to work with a small section of the file.
We don't need to send:
50 GB → ASP.NET Core
Instead:
10 MB → ASP.NET Core
10 MB → ASP.NET Core
10 MB → ASP.NET Core
...
This keeps each HTTP request manageable.
jQuery Implementation
A clean implementation can be divided into three operations:
1. Initialize Upload
2. Upload Chunks
3. Complete Upload
Initialize Upload
First, create an upload session on the server.
async function initializeUpload(file) {
const response = await $.ajax({
url: "/api/uploads",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
fileName: file.name,
fileSize: file.size,
contentType: file.type,
}),
});
return response;
}
The server can return something like:
{
"uploadId": "8f7c...",
"chunkSize": 10485760,
"totalChunks": 5120
}
The uploadId identifies this particular upload.
Upload a Chunk
Each chunk can then be sent separately.
async function uploadChunk(uploadId, chunkNumber, chunk) {
return $.ajax({
url: `/api/uploads/${uploadId}/chunks/${chunkNumber}`,
type: "PUT",
data: chunk,
processData: false,
contentType: "application/octet-stream",
});
}
The important part is:
processData: false;
We want jQuery to send the binary data as-is.
Upload the Complete File
Now we can combine the process:
async function uploadLargeFile(file) {
const upload = await initializeUpload(file);
const chunkSize = 10 * 1024 * 1024;
for (
let start = 0, chunkNumber = 0;
start < file.size;
start += chunkSize, chunkNumber++
) {
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
await uploadChunk(upload.uploadId, chunkNumber, chunk);
const progress = (end / file.size) * 100;
console.log(`Upload Progress: ${progress.toFixed(2)}%`);
}
await completeUpload(upload.uploadId);
}
The key line is:
const chunk = file.slice(start, end);
The browser takes only the required portion of the file for that request.
Handling Failures
The main benefit of chunking appears when something goes wrong.
Suppose we have:
Chunk 0 ✓
Chunk 1 ✓
Chunk 2 ✓
...
Chunk 3499 ✓
Chunk 3500 ✗
Chunk 3501 -
Chunk 3502 -
...
Chunk 5119 -
We don't want to upload chunks 0–3499 again.
Instead, retry chunk 3500.
Chunk 3499 ✓
Chunk 3500 ✗
│
├── Retry
│
├── Retry
│
└── ✓
Chunk 3501 ✓
Chunk 3502 ✓
A simple retry implementation:
async function uploadChunkWithRetry(
uploadId,
chunkNumber,
chunk,
maxRetries = 3,
) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await uploadChunk(uploadId, chunkNumber, chunk);
return;
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, attempt * 2000));
}
}
}
Making the Upload Resumable
Retrying handles temporary failures while the page is still running.
But what happens if:
- the browser is closed?
- the computer restarts?
- the internet goes down for a long time?
- the user comes back later?
This is where resumable upload becomes important.
The server maintains the upload state.
For example:
Upload
--------------------------------
UploadId
FileName
FileSize
ChunkSize
TotalChunks
Status
CreatedAt
And optionally:
UploadChunk
--------------------------------
UploadId
ChunkNumber
Size
Hash
UploadedAt
When the browser reconnects, it can ask:
GET /api/uploads/{uploadId}/status
The server might return:
{
"uploadId": "8f7c...",
"uploadedChunks": [0, 1, 2, 3, 4, 3499]
}
The browser now knows that chunks 0–3499 are already uploaded.
It can continue from:
3500
instead of starting from:
0
The Complete Resumable Flow
Select 50GB File
│
▼
┌─────────────────┐
│ jQuery │
│ Split into │
│ Chunks │
└────────┬────────┘
│
▼
Initialize Upload
│
▼
Upload Chunk #0
│
▼
Upload Chunk #1
│
▼
...
│
▼
Upload Chunk #3500
│
X
Network Failure
│
▼
Check Upload Status
│
▼
Already Uploaded:
0 → 3499
│
▼
Resume from Chunk 3500
│
▼
...
│
▼
Upload Last Chunk
│
▼
Complete Upload
│
▼
Final Azure Blob
ASP.NET Core Side
The API should also work with the request as a stream.
For example:
[HttpPut("{uploadId}/chunks/{chunkNumber}")]
public async Task<IActionResult> UploadChunk(
Guid uploadId,
int chunkNumber,
CancellationToken cancellationToken)
{
await _uploadService.UploadChunkAsync(
uploadId,
chunkNumber,
Request.Body,
cancellationToken);
return Ok();
}
Avoid reading the entire request into memory.
The goal is:
Browser
│
│ 10 MB
▼
ASP.NET Core
│
│ Stream
▼
Azure Function
not:
Browser
│
│ 50 GB
▼
ASP.NET Core Memory
Final Takeaway
For large browser uploads, the main idea is simple:
Don't treat a 50GB file as one HTTP request. Treat it as many smaller, independently retryable requests.
With browser-side chunking, streaming through ASP.NET Core, Azure Function processing, and upload-state tracking, we get:
- Chunked uploads
- Progress tracking
- Automatic retry
- Resumable uploads
- Lower memory usage
- Better handling of network failures
- No need to restart a 50GB upload from zero
For even larger production workloads, another option is to let the browser upload chunks directly to Azure Blob Storage using temporary authorization generated by the backend. This removes the large file traffic from the ASP.NET Core API and Azure Function entirely.
Enjoyed this article? Share it with your network!