So when do you actually reach for a stream instead of just reading the whole thing into memory? What are streams really buying you?
Strong answers treat streams as bounded-memory processing with real flow control, and can do the concurrency-times-payload math that says when you need them
So streams basically let you process data in chunks as it comes in, instead of loading the whole thing into memory first. The classic example is a big file. fs.readFile pulls the entire file into one buffer, so a multi-gigabyte file either fails or just eats that much memory, whereas fs.createReadStream goes piece by piece and the footprint stays small and pretty much constant. You also get to start working on the data before all of it arrives, which helps your time to first byte. And they compose, you can pipe a readable through transforms into a writable. HTTP requests and responses in Node are streams too, actually. And you get backpressure kind of for free, so if a slow consumer can't keep up, the stream slows the producer down instead of buffering forever. My rule of thumb is basically, if the data is large or you don't know the size, use streams.
A stream is really just a bounded queue with a protocol wrapped around it. Chunks, Buffers usually, land in a stage's queue, and highWaterMark is the allowance, 64KiB for byte streams on current Node. Once the queue hits that, the stream tells the producer to stop. So each stage holds about one allowance, and the whole pipeline's memory is the sum of its stages' buffers. A few hundred kilobytes. Doesn't matter if you push terabytes through it. That's the actual purchase here, memory becomes a function of the pipeline's shape instead of the payload's size.
You also get overlap. A buffered read serializes everything, read it all, then process, then write. A pipeline runs all three at once on different chunks, so the first output leaves before the last input even arrives. For a proxy or an export endpoint that turns latency that grows with the payload into near-constant time to first byte.
And the flow control propagates. write() returning false, 'drain', pause/resume, that's how consumer speed travels back up to the producer. With sockets it keeps going into the kernel. Nobody reads the socket, the receive buffer fills, the TCP window closes, and the remote sender just stalls. A slow disk on your end throttles a fast upstream and you wrote zero lines of code for it.
They're not free though. Per-chunk callbacks and queue accounting mean streams lose microbenchmarks against one big read. Error handling and cleanup are a real surface, which is why pipeline exists, hand-wired pipe chains leak. And debugging intermediate state is way harder than staring at one buffer. You're trading code complexity for memory ceilings and TTFB. Great trade at scale, bad trade for a small bounded payload.
At design review the question I actually care about is concurrency times payload against the memory budget. File size on its own settles nothing. A 5MB response body is nothing once, and it's 5GB at a thousand concurrent requests. That product against the container limit is what decides buffered versus streamed. I make teams write the arithmetic into the design doc, because the failure it prevents is memory that scales with traffic, and that failure passes every functional test and then falls over on launch day. Seen it happen.
But streaming has its own failure modes and you own those too. Partial failure mid-stream is the big one. You can't unsend bytes, so an error at chunk 900 of 1000 has no clean HTTP story, the status line went out ages ago. You deal with it through trailers, client-side length checks, or an idempotent re-fetch. Cleanup's next. Every hand-wired pipe chain that skips pipeline semantics is a candidate fd leak when clients disconnect. And slow-loris dynamics, streaming ties per-request resources to the slowest party, so socket timeouts and concurrency caps stop being optional. Your observability shifts too, bytes-in-flight and stream duration replace plain request timing.
And buffering is still right in places. Bounded small payloads under validated caps, single-digit megabytes at realistic concurrency. Data that needs whole-document processing anyway, JSON you parse into one object, a payload you sign and validate as a unit. Code paths where team familiarity beats a marginal memory win. Either way I verify the same way, soak test at production concurrency and watch RSS and arrayBuffers. If the lines are flat under load, the claim holds. That test is a lot cheaper than the incident.
- Plenty of people say 'streams are for large files' and leave it there. File size is only one axis. What actually decides it is concurrency times payload, and that's why a 5MB body can take down a server once enough requests hit it at the same time.
- Watch out for 'streams are faster' said flat out. Per byte they often cost more than a buffered read. What they actually bound is memory and time to first byte, and interviewers notice when you're loose about that.
- 'Use streams everywhere' doesn't land either. For a 2KB config read the stream plumbing buys you nothing, and if you can't admit that, it sounds like you never weighed the tradeoff.