The resumable uploader
Your first Monday at StreamFlow. The CTO pulls you into a war room. Creators are rage-quitting because 8GB uploads die at 98 percent and have to start over. One viral tweet later, the CTO says fix this by Friday.
Constraints are non-negotiable. The server has 512MB of RAM so you cannot buffer the whole file in memory. Storage is local disk. The API must respect the HTTP Content-Range header. Upload sessions expire after 24 hours of inactivity. You must handle a mid-upload server restart without losing progress.
Resumable upload architecture
Client sends chunks with Content-Range. Server writes each chunk to the exact byte offset. On restart, server reads the current file size to know where to resume.
import { createWriteStream } from 'fs';
import { stat } from 'fs/promises';
import { pipeline } from 'stream/promises';
app.patch('/upload/:id', async (req, res) => {
const { id } = req.params;
const range = req.headers['range'] || req.headers['content-range'];
const [start] = parseRange(range);
const path = './uploads/' + id;
const out = createWriteStream(path, { flags: 'a', start });
// pipeline handles backpressure automatically
await pipeline(req, out);
const { size } = await stat(path);
res.json({ uploaded: size });
});Stream the request body straight to a file at the correct byte offset. Memory stays constant.
// Each chunk is its own HTTP request. The id is the upload session.
app.post('/upload/:id/chunk/:index', async (req, res) => {
const { id, index } = req.params;
const chunkPath = './uploads/' + id + '/chunk-' + index;
await pipeline(req, createWriteStream(chunkPath));
await db.markChunkUploaded(id, Number(index));
const missing = await db.getMissingChunks(id);
res.json({ missing });
});For flaky networks, accept one chunk at a time and track progress explicitly so clients can resume even if Content-Range parsing fails.
Two approaches, two tradeoffs. Solution A uses Content-Range and a single append-only file. Simple, fast, and the HTTP standard way. It assumes the client can recover from errors by retrying with a new range. Solution B uses explicit chunking and tracks missing chunks in a database. More moving parts, but it handles deeply unreliable networks where Content-Range based clients struggle. For YouTube at 512MB of RAM, solution A is usually the right answer.
Quiz: Quiz
Loading practice…
Validation checklist: Run the resumable uploader
Loading practice…