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)returnsfalseonce the internal buffer has grown past itshighWaterMark— verified directly, repeatedly, across multiple real fill-and-drain cycles, not a single cherry-picked instance. - When
.write()returnsfalse, 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()andstream.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...ofover 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.