Skip to main content
Node.js Streams

Streams & Buffers

Buffers

JavaScript (in the browser) has historically been poor at handling binary data. Node.js introduced the Buffer class to handle binary data efficiently. A Buffer is a chunk of memory that stores raw binary data. It is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. Why does Node.js need Buffers? JavaScript strings are designed for text (UTF-16 encoded), but servers constantly deal with raw binary data: images, compressed files, TCP packets, cryptographic hashes. Buffers give Node.js the ability to work with this binary data directly, the same way a C program would manipulate raw bytes. Think of a Buffer as a fixed-length array of bytes—once created, its size cannot change.

Creating a Buffer

Streams

Streams are objects that let you read data from a source or write data to a destination in continuous chunks. There are four types of streams:
  1. Readable: Stream you can read from (e.g., fs.createReadStream).
  2. Writable: Stream you can write to (e.g., fs.createWriteStream).
  3. Duplex: Stream that is both Readable and Writable (e.g., net.Socket).
  4. Transform: Stream that can modify or transform the data as it is written and read (e.g., zlib.createGzip).

Why Streams?

If you want to read a massive file (e.g., 2GB video) and send it to a client:
  • Without Streams: You read the entire 2GB into memory. If 100 users do this, your server needs 200GB of RAM—and crashes long before that.
  • With Streams: You read small chunks (e.g., 64KB) and send them one by one. Memory usage remains constant at a few MB regardless of file size or user count.
The Water Pipe Analogy: Think of streams like plumbing. You do not drain an entire lake into a bucket, carry it to your house, and then pour it into the bathtub. Instead, you connect a pipe (stream) from the water source to your faucet. The water flows continuously in small amounts. Piping in Node.js works exactly the same way: readableStream.pipe(writableStream) connects two endpoints, and data flows through automatically. Backpressure is like the faucet being partially closed—if the drain cannot keep up, the pipe slows the flow from the source so nothing overflows.

Readable Stream Example

Piping

Piping is a mechanism where we provide the output of one stream as the input to another stream. It is mainly used to get data from one stream and pass it to another.

Stream Events

Streams are EventEmitters with specific events:

Readable Stream Events

Writable Stream Events

Backpressure Handling

Backpressure occurs when data comes in faster than it can be written. This is one of the most important concepts in stream programming, and ignoring it is a common cause of memory exhaustion in production. If a readable stream produces data at 100MB/s but the writable stream can only consume at 10MB/s, without backpressure handling, the unwritten data piles up in memory until your process runs out of heap space and crashes.

Creating Custom Streams

Custom Readable Stream

Custom Writable Stream

Custom Transform Stream

Practical Example: HTTP File Download

Practical Example: Real-time Log Parser

pipeline() - Better Error Handling

The pipeline() function handles errors and cleanup automatically. This is the recommended way to connect streams in production code. Unlike .pipe(), pipeline() properly destroys all streams if any stream in the chain errors or closes prematurely—preventing resource leaks and orphaned file handles:

Summary

  • Buffers handle raw binary data efficiently—they represent fixed-size chunks of memory outside the V8 heap
  • Streams process data in chunks, enabling memory-efficient operations on files of any size
  • Four stream types: Readable, Writable, Duplex, Transform
  • Piping connects streams and handles backpressure automatically—prefer pipe() over manual data/write event handling
  • Backpressure prevents memory overflow when producer is faster than consumer—always respect the return value of .write()
  • Use pipeline() over .pipe() in production for proper error handling and automatic cleanup of all streams in the chain
  • Create custom streams by extending stream classes—implement _read(), _write(), or _transform() respectively
  • Rule of thumb: If the data fits comfortably in memory (under ~50MB), readFile is fine. If it could be large or you are serving multiple users simultaneously, always use streams