Streams let Node.js process data piece by piece instead of loading it all into memory at once, essential for large files, network responses, and anything where memory efficiency matters. This lesson introduces streams before the dedicated Readable and Writable lessons.
What Are Streams?
A stream represents data that arrives (or is sent) in chunks over time, rather than all at once. Instead of calling fs.readFile() and waiting for an entire file to load into memory, you can open a readable stream and process each chunk as it becomes available, keeping memory usage low regardless of file size.
Node.js has four fundamental stream types: Readable (source of data, like a file being read), Writable (destination for data, like a file being written), Duplex (both readable and writable, like a TCP socket), and Transform (a duplex stream that modifies data as it passes through, like gzip compression).
.pipe() connects a readable stream directly to a writable one, automatically handling chunking and backpressure, without loading the whole file into memory.
The Four Stream Types
Readable - source of data (fs.createReadStream, http response body)
Writable - destination for data (fs.createWriteStream, http request body)
Duplex - both readable and writable (net.Socket)
Transform - duplex that transforms data in transit (zlib.createGzip)
Readable streams emit 'data' events (or support .read()/async iteration) as chunks arrive.
Writable streams expose .write() and .end() to accept chunks.
.pipe(destination) connects a Readable directly to a Writable, handling flow control automatically.
Transform streams sit between a source and destination, modifying each chunk as it passes through.
Streams Cheatsheet
Where streams show up throughout Node.js's core APIs.
Use Case
Stream Type
Example
Reading a large file
Readable
fs.createReadStream(path)
Writing a large file
Writable
fs.createWriteStream(path)
HTTP request body
Readable
req in an http handler
HTTP response body
Writable
res in an http handler
TCP connection
Duplex
net.Socket
Gzip compression
Transform
zlib.createGzip()
Why Streams Matter for Memory Efficiency
Loading an entire 2GB file into memory with fs.readFile() requires roughly 2GB of RAM (often more, accounting for encoding overhead) before you can do anything with it. A stream instead processes the file in small chunks, typically 64KB by default, keeping memory usage flat regardless of the total file size.
Chaining Streams Together
Because .pipe() returns the destination stream, you can chain multiple streams together, source, through one or more transforms, to a final destination, composing complex data pipelines from small, reusable pieces.
import fs from 'node:fs';
import zlib from 'node:zlib';
fs.createReadStream('./access.log')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('./access.log.gz'));
This compresses a log file on the fly, streaming through gzip, without ever holding the full uncompressed or compressed file in memory at once.
Common Mistakes
Using fs.readFile() for very large files instead of streaming, causing high memory usage.
Forgetting to handle the 'error' event on streams, letting failures crash the process silently.
Chaining .pipe() without checking for backpressure-related warnings on slower writable destinations.
Assuming all streams are files, streams also represent network sockets, HTTP bodies, and compressed data.
Key Takeaways
Streams process data in chunks over time instead of loading everything into memory at once.
The four stream types are Readable, Writable, Duplex, and Transform.
.pipe() connects streams together and manages flow control automatically.
Streams are essential for large files, network I/O, and real-time data processing.
Pro Tip
Whenever you catch yourself calling fs.readFile() on something that could be large or unbounded (uploads, logs, exports), pause and consider whether a stream-based approach would keep memory usage predictable instead.
You now understand streams conceptually. Next, go deeper on Readable Streams specifically.