GB.
2026-07-097 min read

Upload 50GB Files from the Browser

#.NET#Chunked Upload#Resumable Upload#jQuery#Azure Functions#Azure Blob Storage#File Upload

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!