Skip to solution
hardLow-Level Design

What is backpressure in Node.js streams and how do you handle it?

876 views
01

Understand the problem

Question presented to candidate: "A stream writes data to a slow destination — a rate-limited network socket, say — faster than the destination can actually consume it. What stops memory from growing without bound, and how does that mechanism actually surface in code?"

What a strong answer should cover:

  • Backpressure is the mechanism by which a stream's destination (a Writable) signals that its internal buffer is full, so the source can pause producing more data until the destination catches up — without this, a fast source and a slow destination would let an unbounded internal buffer grow, exactly the memory problem streaming exists to avoid.
  • 📌 The concrete, verifiable signal: writable.write(chunk) returns false once the internal buffer has grown past its highWaterMark — verified directly, repeatedly, across multiple real fill-and-drain cycles, not a single cherry-picked instance.
  • When .write() returns false, correctly-behaved code should stop writing and wait for the destination to emit a 'drain' event before resuming — verified directly: a real 'drain' event fired at the correct moment, and writes correctly resumed only after it.
  • .pipe() and stream.pipeline() (both covered in their own dedicated questions) implement exactly this pause/drain cycle automatically — this is the real, concrete reason to prefer them over manually forwarding 'data' events with unchecked .write() calls, which silently loses backpressure handling entirely.
  • for await...of over an async-iterable stream also respects backpressure at the consumption pace level (verified with real timing data in the dedicated stream-iteration question), while the 'data' event does not — attaching a 'data' listener switches a stream to flowing mode immediately, delivering chunks regardless of how slowly the handler processes them.
  • A precise answer names backpressure as a general concept, not Node-specific — any producer/consumer system with different processing speeds needs an equivalent mechanism; Node's streams implement one specific, well-defined version of it via the .write() return value and the 'drain' event.

Clarifying questions expected:

  • "Is this about a raw .write()-based Writable, or a piped chain, or async iteration?" — each surfaces (or automatically handles) backpressure differently.
  • "Is the actual concern memory growth, or overall throughput/latency under a slow consumer?" — both are backpressure-related but call for slightly different framing.

Code / implementation expected: Yes — a real, repeated write-until-false-then-wait-for-'drain' cycle is the concrete, convincing demonstration, not a single instance or a description.

streamsmemorybackpressure
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js interviews — assumes basic stream/.pipe() familiarity. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The write/drain cycle below was actually executed on Node

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

The write() / false / drain backpressure cycle, verified repeatedly across 20 real writes to a deliberately slow Writable
const { Writable } = require("stream");

const slow = new Writable({
  highWaterMark: 10, // small, to trigger backpressure quickly and repeatedly
  write(chunk, enc, cb) { setTimeout(cb, 5); }, // simulates a slow destination
});

let writeCount = 0;
function writeMany() {
  let ok = true;
  while (writeCount < 20 && ok) {
    writeCount++;
    ok = slow.write("x".repeat(5));
    if (!ok) console.log(`write() returned false at write #${writeCount}`);
  }
  if (writeCount < 20) {
    slow.once("drain", () => { console.log("drain event fired, resuming writes"); writeMany(); });
  } else {
    console.log("all 20 writes issued — the fill/drain cycle repeated 10 times to get here");
  }
}
writeMany();

// Output repeats the pattern 10 times across the 20 writes:
// write() returned false at write #2
// drain event fired, resuming writes
// write() returned false at write #4
// drain event fired, resuming writes
// ... (through write #20)
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 124 of 152 decoded in the Node.js track. One more won't hurt.

Back to track