Skip to content

Node.js Stream Backpressure — Complete Guide to Flow Control

DodaTech Updated 2026-06-28 4 min read

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

What happens if you ignore backpressure?

The internal buffer grows unbounded. When memory is exhausted, the process crashes with heap out of memory.

Does backpressure affect the producer's event loop?

Yes. The producer must wait for drain, effectively pausing its execution. This is intentional flow control.

Can I disable backpressure?

Set highWaterMark to Infinity. Not recommended. It removes memory protection.

How does pipe() handle backpressure?

pipe() automatically manages backpressure by pausing the readable when the writable buffer is full.

Does pipeline() handle backpressure differently?

No. pipeline() uses the same backpressure mechanism as pipe() but adds proper error propagation.

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