Chapter 56 โ Streams, Buffers & Backpressure
๐ Definition
A Buffer is a fixed-size chunk of raw binary memory outside the V8 heap, used for bytes (files, network packets, images). A stream processes data piece by piece (in chunks) instead of loading it all into memory. Backpressure is the mechanism that slows a fast producer down when the consumer cannot keep up.
๐ Explanation
Four stream types
| Type | Direction | Examples |
|---|---|---|
| Readable | Source | fs.createReadStream, req (incoming HTTP body), MongoDB cursor stream |
| Writable | Destination | fs.createWriteStream, res (HTTP response), S3 upload |
| Duplex | Both, independent | TCP socket, WebSocket |
| Transform | Both, output derived from input | zlib.createGzip(), CSV parser, encryption |
Why streams?
// โ Loads a 2 GB file into memory โ crashes or slows the whole server
app.get("/video", async (req, res) => {
const data = await fs.promises.readFile("movie.mp4");
res.end(data);
});
// โ
Constant ~64 KB memory regardless of file size
app.get("/video", (req, res) => {
fs.createReadStream("movie.mp4").pipe(res);
});Backpressure
writable.write(chunk) returns false when the writable's internal buffer exceeds its highWaterMark. A correct producer then pauses until the drain event. pipe() and pipeline() do this automatically โ manual on("data") + write() loops often do not, and memory explodes.
๐ป Code Example โ Buffers
const buf = Buffer.from("Hello", "utf8");
buf; // <Buffer 48 65 6c 6c 6f>
buf.toString("base64"); // "SGVsbG8="
buf.length; // 5 (bytes, not characters)
Buffer.byteLength("โน"); // 3 โ multi-byte UTF-8
Buffer.alloc(10); // zero-filled
Buffer.concat([b1, b2]);๐ป Code Example โ pipeline With Error Handling
import { pipeline } from "node:stream/promises";
import fs from "node:fs";
import zlib from "node:zlib";
await pipeline(
fs.createReadStream("access.log"),
zlib.createGzip(),
fs.createWriteStream("access.log.gz"),
);
// pipeline handles backpressure AND destroys every stream if one fails.
// .pipe() does not propagate errors โ prefer pipeline in production.๐ป Code Example โ Custom Transform Stream
import { Transform } from "node:stream";
const upperCase = new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
await pipeline(process.stdin, upperCase, process.stdout);๐ป Code Example โ Export 1 Million Orders as CSV (No Memory Spike)
import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
app.get("/api/reports/orders.csv", async (req, res, next) => {
res.setHeader("Content-Type", "text/csv");
res.setHeader("Content-Disposition", 'attachment; filename="orders.csv"');
const toCsv = new Transform({
objectMode: true, // receives objects, emits strings
transform(order, _enc, cb) {
cb(null, `${order._id},${order.customer},${order.total},${order.createdAt.toISOString()}\n`);
},
});
res.write("id,customer,total,createdAt\n");
try {
await pipeline(
Order.find({ status: "paid" }).select("customer total createdAt").lean().cursor(),
toCsv,
res,
);
} catch (err) {
next(err);
}
});๐ป Code Example โ Import a Large CSV Into MongoDB in Batches
import { parse } from "csv-parse";
async function importProducts(path) {
const parser = fs.createReadStream(path).pipe(parse({ columns: true }));
let batch = [];
for await (const row of parser) { // async iteration respects backpressure
batch.push({ name: row.name, price: Number(row.price) });
if (batch.length === 1000) {
await Product.insertMany(batch, { ordered: false });
batch = [];
}
}
if (batch.length) await Product.insertMany(batch, { ordered: false });
}๐ป Code Example โ Manual Backpressure
function writeMany(writable, lines) {
let i = 0;
function write() {
let ok = true;
while (i < lines.length && ok) {
ok = writable.write(lines[i++] + "\n"); // false โ buffer is full
}
if (i < lines.length) writable.once("drain", write); // resume when drained
else writable.end();
}
write();
}๐ป Code Example โ Streaming Upload to S3
import { Upload } from "@aws-sdk/lib-storage";
import busboy from "busboy";
app.post("/upload", (req, res, next) => {
const bb = busboy({ headers: req.headers, limits: { fileSize: 500 * 1024 * 1024 } });
bb.on("file", async (_name, file, info) => {
try {
const upload = new Upload({
client: s3,
params: { Bucket: process.env.BUCKET, Key: `uploads/${Date.now()}-${info.filename}`, Body: file },
});
const result = await upload.done(); // multipart upload, streamed
res.json({ url: result.Location });
} catch (err) { next(err); }
});
req.pipe(bb);
});The file never touches the server's disk or sits in memory in full.
๐ Object Mode and highWaterMark
- Default streams carry Buffers/strings;
highWaterMarkis in bytes (64 KB for fs read streams, 16 KB otherwise). objectMode: truestreams carry JS objects;highWaterMarkcounts objects (default 16).
๐ Real-World Use
- Video/audio streaming with HTTP
Rangerequests. - CSV/Excel exports and imports of large datasets.
- Proxying uploads straight to S3.
- Log processing, compression, encryption on the fly.
- Server-Sent Events and streaming AI responses (
res.writechunks).
๐ฏ Likely Interview Questions
- What is a stream? Name the four types with examples.
- Why use streams instead of
fs.readFilefor large files? - What is backpressure and how does Node handle it?
pipe()vspipeline()? โpipelinepropagates errors and cleans up.- What is a Buffer? Why is
Buffer.lengthdifferent fromstring.length? - How would you export 1 million MongoDB documents as CSV without running out of memory?
- What is
objectMode? - How would you upload a 2 GB file through your API?
โ Rate Limiting | Index | Next: Cluster, Worker Threads & Scaling โ