Skip to content
>_ TrueFileSize.com
·11 min read

Chunked Video Upload with Resumable Transfers

Uploading a 500MB video over hotel wifi and having it fail at 98% is the classic UX disaster. The fix is chunked, resumable upload. This guide covers implementation, the tus protocol, and how to test with real large files.

Why chunked uploads

  • Single-shot uploads hit server/proxy timeout limits (typically 60-300 seconds)
  • No resume — a 99% upload that fails starts from zero
  • Browsers buffer the whole file in memory, crashing on mobile
  • Progress tracking is all-or-nothing

Chunked upload in plain JavaScript

async function uploadChunks(file, endpoint) {
  const CHUNK_SIZE = 5 * 1024 * 1024;  // 5MB
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  const uploadId = crypto.randomUUID();

  for (let i = 0; i < totalChunks; i++) {
    const start = i * CHUNK_SIZE;
    const chunk = file.slice(start, start + CHUNK_SIZE);
    const form = new FormData();
    form.append('chunk', chunk);
    form.append('uploadId', uploadId);
    form.append('index', String(i));
    form.append('total', String(totalChunks));

    await fetch(endpoint, { method: 'POST', body: form });
    reportProgress((i + 1) / totalChunks);
  }
  await fetch(endpoint + '/finalize', {
    method: 'POST',
    body: JSON.stringify({ uploadId, filename: file.name }),
  });
}

Server-side chunk assembly (Node.js)

// On chunk receive: append to a temp file named by uploadId
fs.appendFileSync(`/tmp/${uploadId}`, chunk);

// On finalize: verify total bytes, move to final location
const stats = fs.statSync(`/tmp/${uploadId}`);
if (stats.size !== expectedSize) throw new Error('Size mismatch');
fs.renameSync(`/tmp/${uploadId}`, `/videos/${filename}`);

Resumable uploads with tus

For production, don't hand-roll chunking — use the tus protocol. It handles resumption, parallel chunks, and checksums:

import * as tus from 'tus-js-client';

const upload = new tus.Upload(file, {
  endpoint: '/api/tus/',
  retryDelays: [0, 3000, 6000, 12000, 30000],
  chunkSize: 5 * 1024 * 1024,
  metadata: { filename: file.name, filetype: file.type },
  onProgress(bytesUploaded, bytesTotal) {
    const pct = (bytesUploaded / bytesTotal * 100).toFixed(1);
    console.log(pct + '%');
  },
  onSuccess() {
    console.log('Done:', upload.url);
  },
});
upload.start();

Testing with sample videos

Validate your implementation across size tiers:

Test checklist

  • 500MB upload completes without memory growth above 50MB
  • Disconnect Wi-Fi mid-upload, reconnect, upload resumes from last chunk
  • Progress bar updates smoothly (not jumping by 20% at a time)
  • Cancel button frees all server-side temp state
  • Two simultaneous uploads don't corrupt each other
  • Server rejects chunks with wrong checksum

Related

For the protocol deep-dive, read resumable uploads with tus. For streaming the video back, see HLS vs MP4 streaming.