Node.js Streams Cheat Sheet

Backpressure, pipeline and why you should stop using .pipe()

Streams process data without holding it in memory. They are also where Node applications leak, hang and lose errors — almost always for the same three reasons.

Category: Node.js Difficulty: Intermediate Version: 1.0 Updated: June 6, 2025 Author: Sabir

Types

Type Does Example
Readable Produces data fs.createReadStream, an HTTP request
Writable Consumes data fs.createWriteStream, an HTTP response
Duplex Both, independently A TCP socket
Transform Both, output derived from input zlib.createGzip

Composition

Pattern Note
await pipeline(src, transform, dest) The right default — propagates errors and cleans up
src.pipe(dest) Does NOT forward errors and leaks on failure
for await (const chunk of readable) Async iteration; honours backpressure automatically
Readable.from(asyncGenerator()) Turn any async generator into a stream
stream.destroy(err) Tear down and propagate

Backpressure

writable.write() returns false when its buffer is full. Ignoring that return value is how a fast reader and a slow writer turn into unbounded memory growth — the data has to go somewhere, and it goes into RAM. pipeline() and for-await both handle this for you; a hand-rolled loop must wait for the "drain" event itself.

Code examples

pipeline() with a transform JavaScript

The composition that propagates errors and cleans up. .pipe() does neither.

import { pipeline } from 'node:stream/promises';
import { Transform } from 'node:stream';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';

const upper = new Transform({
  transform(chunk, _enc, cb) {
    cb(null, chunk.toString().toUpperCase());
  },
});

try {
  await pipeline(
    createReadStream('in.txt'),
    upper,
    createGzip(),
    createWriteStream('out.txt.gz'),
  );
  console.log('done');
} catch (err) {
  console.error('stream failed:', err);   // reachable, unlike with .pipe()
}

FAQs

Why should I stop using .pipe()?

It does not forward errors and does not clean up on failure, so a broken destination leaks the source handle and your catch block never runs. pipeline() fixes both.

What is backpressure, practically?

write() returning false means the buffer is full. Ignoring it means the unread data piles up in memory. pipeline() and for-await both handle it; a hand-rolled loop must wait for "drain".