Web APIsJavaScriptMedium

How does Blob.slice() work for chunked file uploads?

01

The Short Answer

Blob.slice() lets you cut a large Blob (or File) into smaller pieces without copying the data. This is the foundation of chunked file uploads — instead of sending a 500MB file in one request that might timeout or fail, you split it into manageable chunks (say 5MB each) and upload them individually. If one chunk fails, you only retry that chunk, not the entire file.

02

How slice() Works

blob.slice(start, end) returns a new Blob representing bytes from start up to (but not including) end. It works like Array.slice() — it creates a lightweight view, not a copy of the data.

slice-basics.tstypescript
// A 100-byte blob
const blob = new Blob(['A'.repeat(100)]);

// Slice into chunks of 25 bytes
const chunk1 = blob.slice(0, 25);    // bytes 0-24
const chunk2 = blob.slice(25, 50);   // bytes 25-49
const chunk3 = blob.slice(50, 75);   // bytes 50-74
const chunk4 = blob.slice(75, 100);  // bytes 75-99

// Each chunk is a new Blob
console.log(chunk1.size); // 25
console.log(chunk1 instanceof Blob); // true

// The original blob is unchanged
console.log(blob.size); // 100

// Negative indices work (like Array.slice)
const lastTen = blob.slice(-10); // last 10 bytes

No data copying

Slicing a 500MB file into 100 chunks does NOT create 500MB of copies. Each slice is a lightweight reference to a portion of the original data. This is why chunked uploads don't double your memory usage.

03

Basic Chunked Upload

Here's the simplest chunked upload implementation — split the file and upload each piece sequentially:

basic-chunked-upload.tstypescript
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB per chunk

async function uploadFile(file: File) {
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);

  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('chunkIndex', String(i));
    formData.append('totalChunks', String(totalChunks));
    formData.append('fileName', file.name);

    await fetch('/api/upload-chunk', {
      method: 'POST',
      body: formData
    });

    console.log(`Uploaded chunk ${i + 1}/${totalChunks}`);
  }

  // Tell the server all chunks are uploaded
  await fetch('/api/upload-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileName: file.name, totalChunks })
  });
}
04

Chunked Upload with Progress Tracking

upload-with-progress.tstypescript
type UploadProgress = {
  percent: number;
  uploadedBytes: number;
  totalBytes: number;
  currentChunk: number;
  totalChunks: number;
};

async function uploadWithProgress(
  file: File,
  onProgress: (progress: UploadProgress) => void
) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  let uploadedBytes = 0;

  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('chunkIndex', String(i));
    formData.append('totalChunks', String(totalChunks));
    formData.append('fileName', file.name);

    await fetch('/api/upload-chunk', { method: 'POST', body: formData });

    uploadedBytes += chunk.size;
    onProgress({
      percent: Math.round((uploadedBytes / file.size) * 100),
      uploadedBytes,
      totalBytes: file.size,
      currentChunk: i + 1,
      totalChunks
    });
  }
}

// Usage
uploadWithProgress(file, (progress) => {
  progressBar.style.width = `${progress.percent}%`;
  statusText.textContent = `${progress.percent}% (${progress.currentChunk}/${progress.totalChunks} chunks)`;
});
05

Resumable Upload

The real power of chunked uploads: if the connection drops mid-upload, you can resume from where you left off instead of starting over. The server tracks which chunks it received, and the client skips those.

resumable-upload.tstypescript
async function resumableUpload(file: File) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);

  // Ask the server which chunks it already has
  const res = await fetch(`/api/upload-status?fileName=${file.name}`);
  const { uploadedChunks } = await res.json() as { uploadedChunks: number[] };
  // e.g., [0, 1, 2] means first 3 chunks are already uploaded

  for (let i = 0; i < totalChunks; i++) {
    // Skip chunks the server already has
    if (uploadedChunks.includes(i)) {
      console.log(`Chunk ${i} already uploaded, skipping`);
      continue;
    }

    const start = i * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, file.size);
    const chunk = file.slice(start, end);

    await uploadChunkWithRetry(chunk, i, totalChunks, file.name);
  }

  await fetch('/api/upload-complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ fileName: file.name, totalChunks })
  });
}

async function uploadChunkWithRetry(
  chunk: Blob,
  index: number,
  total: number,
  fileName: string,
  maxRetries = 3
) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const formData = new FormData();
      formData.append('chunk', chunk);
      formData.append('chunkIndex', String(index));
      formData.append('totalChunks', String(total));
      formData.append('fileName', fileName);

      const res = await fetch('/api/upload-chunk', { method: 'POST', body: formData });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return; // Success
    } catch (err) {
      if (attempt === maxRetries) throw err;
      console.log(`Chunk ${index} failed, retrying (${attempt}/${maxRetries})...`);
      await new Promise(r => setTimeout(r, 1000 * attempt)); // Exponential backoff
    }
  }
}

Why this matters

On mobile networks or flaky connections, uploading a 200MB file without chunking means any hiccup restarts the entire upload. With chunked + resumable uploads, users never lose progress. This is how Google Drive, Dropbox, and YouTube handle large file uploads.

06

Parallel Chunk Uploads

Sequential uploads are reliable but slow. You can upload multiple chunks in parallel to maximize bandwidth, while limiting concurrency to avoid overwhelming the server:

parallel-upload.tstypescript
async function parallelChunkedUpload(file: File, concurrency = 3) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);

  // Create all chunk tasks
  const chunks = Array.from({ length: totalChunks }, (_, i) => ({
    index: i,
    blob: file.slice(i * CHUNK_SIZE, Math.min((i + 1) * CHUNK_SIZE, file.size))
  }));

  // Upload with limited concurrency
  const inFlight = new Set<Promise<void>>();

  for (const chunk of chunks) {
    const task = uploadSingleChunk(chunk.blob, chunk.index, totalChunks, file.name)
      .then(() => { inFlight.delete(task); });

    inFlight.add(task);

    // If we hit the concurrency limit, wait for one to finish
    if (inFlight.size >= concurrency) {
      await Promise.race(inFlight);
    }
  }

  // Wait for all remaining uploads
  await Promise.all(inFlight);
}

async function uploadSingleChunk(chunk: Blob, index: number, total: number, fileName: string) {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('chunkIndex', String(index));
  formData.append('totalChunks', String(total));
  formData.append('fileName', fileName);
  await fetch('/api/upload-chunk', { method: 'POST', body: formData });
}
07

Common Mistakes

📐

Wrong chunk size

Too small (10KB) = thousands of HTTP requests with overhead. Too large (100MB) = defeats the purpose of chunking, timeouts still likely.

Use 1-10MB chunks. 5MB is a common sweet spot that balances request overhead vs failure recovery.

🔢

Off-by-one on the last chunk

The last chunk is usually smaller than CHUNK_SIZE. Forgetting to cap `end` at `file.size` can cause issues.

Always use `Math.min(start + CHUNK_SIZE, file.size)` for the end boundary.

🆔

No unique upload identifier

If two users upload a file with the same name simultaneously, chunks get mixed up on the server.

Generate a unique upload ID (UUID) per upload session and include it with every chunk request.

08

Why Interviewers Ask This

Chunked file upload is a classic system design question for frontend engineers. It tests whether you understand large file handling, network reliability, progress tracking, and concurrency control. It also comes up in machine coding rounds where you might need to implement it from scratch.

Quick Revision Cheat Sheet

blob.slice(start, end): Creates a lightweight view of a portion — no data copy

Chunk size: 1-10MB typical, 5MB is a common default

Resumable uploads: Ask server which chunks exist, skip those, retry only failed ones

Parallel uploads: Upload multiple chunks concurrently with a concurrency limit

Progress tracking: Track uploaded bytes per chunk completion

Real-world examples: Google Drive, Dropbox, YouTube, S3 multipart uploads