Streams & Buffers
Node.js Intermediate
Chapter 1 of 8 · Streams & Buffers
Streams & Buffers
Course 1 showed you how to read files with fs.readFile and fs.readFileSync. These work fine for small files — but try loading a 4 GB log file and Node reads the entire thing into memory before you can touch it. Streams solve this by processing data in small chunks as it arrives, keeping memory usage flat regardless of file size.
Buffers — Raw Binary Data in Node
Before streams make sense, you need to understand Buffers. A Buffer is Node's representation of raw binary data — a fixed-size chunk of memory outside the V8 heap. When a stream hands you data, it's a Buffer by default.
💡 Why Not Just Use Strings?
Strings in V8 are UTF-16 encoded. Binary data — images, compressed files, network packets — doesn't map cleanly to strings and will be corrupted by encoding conversions. Buffers give you direct byte access. When you need text, call .toString('utf8') yourself.
The Four Stream Types
Readable
A source of data you consume from. Examples: fs.createReadStream, http.IncomingMessage, process.stdin.
Writable
A destination you write data into. Examples: fs.createWriteStream, http.ServerResponse, process.stdout.
Duplex
Both readable and writable, but independently. A TCP socket is a classic example — you read incoming data and write outgoing data on the same socket.
Transform
A Duplex where output is derived from input. Examples: zlib.createGzip(), CSV parsers, encryption streams — data goes in one form, comes out another.
Readable Streams
Readable streams have two modes: flowing (data events fire automatically) and paused (you pull chunks manually). Attaching a data event listener switches to flowing mode.
Writable Streams
You push data in with write() and signal completion with end(). The return value of write() is critical — false means the internal buffer is full and you should pause.
Transform Streams
Transform streams sit between a readable and a writable, modifying data as it passes through. You implement _transform() to process each chunk and _flush() for any cleanup at the end.
⚠️ Always Call callback() in _transform()
Forgetting to call callback() stalls the stream silently — no error, no data, no progress. If processing fails, pass the error: callback(err). This is the most common transform stream bug.
pipe() and pipeline()
pipe() connects a readable to a writable and manages the data flow loop for you. pipeline() (from stream/promises, Node 10+) is the modern replacement — it properly destroys all streams if any one of them errors, preventing resource leaks.
Backpressure
Backpressure is what happens when a fast readable outpaces a slow writable — the internal buffer fills, memory climbs, and the whole point of streaming is defeated. pipe() and pipeline() handle this automatically by pausing the readable when the writable's buffer is full and resuming when it drains.
Quick Reference
| Concept | Key API | When to use |
|---|---|---|
Buffer |
Buffer.alloc(n), Buffer.from(), .toString() |
Working with binary data or explicit encoding conversions |
| Readable stream | fs.createReadStream(), on('data'), for await |
Reading large files or network data chunk by chunk |
| Writable stream | fs.createWriteStream(), write(), end() |
Writing output without building it entirely in memory |
| Transform stream | extends Transform, _transform(chunk, enc, cb) |
Modifying data in-flight: compress, encrypt, parse, convert |
pipe() |
readable.pipe(writable) |
Quick connections — attach error handlers manually |
pipeline() |
await pipeline(r, t, w) |
Production use — automatic cleanup on error, async/await friendly |
| Backpressure | write() → false, 'drain' event |
Handled automatically by pipe() / pipeline() |
⚠️ Common Mistakes
Using pipe() in production without error handling — if the writable errors, the readable keeps flowing and memory climbs. Use pipeline() or attach 'error' listeners to every stream.
Forgetting callback() in _transform() — the stream stalls silently. No error message, no data flowing, no indication why. Always call callback() or callback(err).
Mixing event listeners and async iteration on the same stream — attaching a 'data' listener switches to flowing mode and will conflict with for await...of. Pick one approach per stream.
Coding Challenges
Challenge 1 — Line-by-Line File Reader
Write a function countLines(filePath) that uses a readable stream (not fs.readFile) to count the number of lines in a file. Handle the edge case where the final chunk doesn't end with a newline. The function should return a Promise that resolves with the total line count.
Challenge 2 — Custom Transform: Word Censor
Build a CensorTransform class that extends Transform. It should accept an array of words to censor in its constructor, and replace each occurrence of those words in the stream with *** (case-insensitive). Test it by piping a readable through it to process.stdout.
Challenge 3 — File Compressor with pipeline()
Write an async function compressFile(inputPath) that uses pipeline() to gzip-compress a file, saving it as inputPath + '.gz'. After compression, log the original size, compressed size, and percentage reduction. Handle errors with a clear error message and non-zero exit code.