·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:
- 10MB MP4 — smoke test, completes in 1-2 chunks
- 100MB MP4 — real-world test
- 500MB MP4 — stress test, exercises pause/resume
- 1GB binary — extreme edge case
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.