Node.js streams: memory-safe handling of large uploads
Why buffering entire request bodies fails at scale, and how pipeline and backpressure keep the process stable under load.
Holding a multi-gigabyte upload in a single Buffer is a predictable path to GC pressure, OOM risk, and tail latency. Node’s stream APIs let you process data incrementally and respect backpressure so producers slow when consumers are saturated.
Prefer stream/promises pipeline
pipeline wires readable → writable, propagates errors, and handles drain correctly—safer than manual pipe in production code.
import type { IncomingMessage, ServerResponse } from "http";
import { createWriteStream } from "fs";
import { pipeline } from "stream/promises";
export async function persistUpload(
req: IncomingMessage,
res: ServerResponse,
filePath: string,
) {
try {
await pipeline(req, createWriteStream(filePath));
res.writeHead(201, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true }));
} catch {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: false, error: "upload_failed" }));
}
}
Policy at the edge
Configure reverse proxies (client_max_body_size or equivalent) to match application limits. Without alignment, failures surface as opaque disconnects instead of a controlled 413.
Takeaway: Streams are not an optimization—they are how you bound memory per request while preserving throughput.