Streams & Buffers
Buffers
JavaScript (in the browser) has historically been poor at handling binary data. Node.js introduced theBuffer 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:- Readable: Stream you can read from (e.g.,
fs.createReadStream). - Writable: Stream you can write to (e.g.,
fs.createWriteStream). - Duplex: Stream that is both Readable and Writable (e.g.,
net.Socket). - 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.
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
Thepipeline() 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 manualdata/writeevent 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),
readFileis fine. If it could be large or you are serving multiple users simultaneously, always use streams