Node.js Stream Backpressure — Complete Guide to Flow Control
In this tutorial, you will learn about Node.js Stream Backpressure. We cover key concepts, practical examples, and best practices to help you master this topic.
Node.js stream backpressure is the mechanism that slows down data production when the consumer cannot keep up, preventing unbounded memory growth through internal buffer controls.
What You'll Learn
By the end of this tutorial, you'll understand backpressure mechanics, configure highWaterMark, handle drain events, use cork/uncork, and build backpressure-aware streaming applications.
Why Backpressure Matters
Without backpressure, fast producers overwhelm slow consumers, causing memory to grow until the Process crashes or hits the max memory limit.
Real-World Use
A file upload server receives data faster than it writes to disk. Backpressure throttles the incoming request, pausing the TCP socket until the disk catches up, preventing memory exhaustion.
Backpressure Path
flowchart LR
A[Streams Deep] --> B[Backpressure]
B --> C[Pipeline]
C --> D[Buffers]
D --> E[Error Handling]
B --> F{You Are Here}
style F fill:#f90,color:#fff
How Backpressure Works
When a Writable stream's internal buffer exceeds highWaterMark, write() returns false. The producer should wait for the drain event before writing more.
const { Writable } = require("node:stream");
const fs = require("node:fs");
const writable = fs.createWriteStream("/tmp/large-file.txt", { highWaterMark: 16384 });
function writeData(data) {
const canContinue = writable.write(data);
if (!canContinue) {
console.log("Backpressure: pausing writes");
writable.once("drain", () => {
console.log("Drain: resuming writes");
writeMore();
});
}
}
The highWaterMark Option
highWaterMark sets the threshold for the internal buffer. When buffered bytes exceed this, backpressure triggers.
const { Readable, Writable } = require("node:stream");
const readable = new Readable({ highWaterMark: 1024 });
const writable = new Writable({ highWaterMark: 2048 });
console.log("Readable highWaterMark:", readable.readableHighWaterMark);
console.log("Writable highWaterMark:", writable.writableHighWaterMark);
console.log("Readable buffer length:", readable.readableLength);
console.log("Writable buffer length:", writable.writableLength);
Handling Drain Events
The drain event signals that the writable buffer has emptied below highWaterMark. Resume writing after drain.
const { Writable } = require("node:stream");
let writeCount = 0;
const slowWriter = new Writable({
highWaterMark: 256,
write(chunk, enc, cb) {
setTimeout(() => {
writeCount++;
cb();
}, 100);
},
});
function writeLots() {
let drained = true;
while (drained) {
drained = slowWriter.write(Buffer.alloc(64));
if (!drained) {
console.log("Buffer full, waiting for drain...");
slowWriter.once("drain", () => {
console.log("Drained, continuing");
writeLots();
});
}
}
}
writeLots();
Cork and Uncork
cork() forces buffering of all writes until uncork() is called. This batches small writes for efficiency.
const { Writable } = require("node:stream");
const fs = require("node:fs");
const ws = fs.createWriteStream("/tmp/batched.txt");
ws.cork();
ws.write("Batch ");
ws.write("of ");
ws.write("small ");
ws.write("writes ");
ws.write("in one flush");
process.nextTick(() => ws.uncork());
// All writes are flushed in a single operation after uncork
Readable Backpressure
Readable streams also have backpressure. When push() returns false, the internal buffer is full. Stop pushing until more is read.
const { Readable } = require("node:stream");
const backpressureSource = new Readable({
highWaterMark: 256,
read() {
const data = Buffer.alloc(64);
const canPush = this.push(data);
if (!canPush) {
console.log("Readable buffer full, stopping push");
} else {
console.log("Pushed data, buffer length:", this.readableLength);
}
},
});
backpressureSource.on("data", () => {});
Common Mistakes
1. Ignoring the Return Value of write()
Always check the return value of write(). If false, wait for drain before continuing.
2. Setting highWaterMark Too High
Large buffers delay backpressure. A 64MB buffer means 64MB of memory per stream before throttling.
3. Not Using cork/uncork for Batch Writes
Writing single bytes without cork causes excessive I/O operations. Cork batches small writes.
4. Forgetting to Remove Drain Listeners
Drain listeners accumulate if not removed with once(). This causes multiple firings.
5. Misunderstanding Object Mode Backpressure
In object mode, highWaterMark counts objects, not bytes. 16 objects default. Adjust for object payload size.
Practice Questions
1. What does write() return and what does it mean?
Returns false when the internal buffer exceeds highWaterMark. True means more data can be written safely.
2. What is the drain event?
Emitted when the writable buffer has emptied below the highWaterMark, signaling it is safe to resume writing.
3. How does cork/uncork help with performance?
cork buffers all writes until uncork, flushing them as a single operation. Reduces I/O for small frequent writes.
4. What is the default highWaterMark for Writable streams?
16384 bytes (16KB). For object mode, the default is 16 objects.
5. Challenge: Create a backpressure-aware stream that logs buffer utilization.
class MonitoredWritable extends Writable {
_write(chunk, enc, cb) {
console.log(`Buffer at ${((this.writableLength / this.writableHighWaterMark) * 100).toFixed(0)}%`);
setTimeout(cb, 50);
}
}
const mw = new MonitoredWritable({ highWaterMark: 1024 });
for (let i = 0; i < 50; i++) mw.write(Buffer.alloc(256));
FAQ
Mini Project: Backpressure Monitor
Build a stream wrapper that logs backpressure events with timing.
class BackpressureMonitor extends Writable {
constructor(options = {}) {
super(options);
this.backpressureStart = null;
this.totalBackpressureMs = 0;
this.on("drain", () => {
if (this.backpressureStart) {
this.totalBackpressureMs += Date.now() - this.backpressureStart;
this.backpressureStart = null;
}
});
}
_write(chunk, enc, cb) {
if (this.writableLength >= this.writableHighWaterMark) {
this.backpressureStart ??= Date.now();
}
setTimeout(cb, 10);
}
}
What's Next
Node.js Stream Pipeline Node.js Buffers Node.js Error Handling
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro