Process massive amounts of data piece by piece. Memory-efficient, time-efficient, and incredibly powerful.
Traditional methods load everything into memory. Streams process data as it flows, enabling you to work with files of any size.
Process multi-GB files with constant memory usage. No more "JavaScript heap out of memory" errors.
Start processing immediately. Don't wait for the entire file to load before beginning work.
Chain streams together with pipes. Read β Compress β Encrypt β Write in one flowing pipeline.
Videos, logs, databases, network responses. If it's big, streams handle it gracefully.
Node.js provides four fundamental stream types, each designed for specific data flow patterns. Understanding these is key to mastering data processing.
Think of streams like plumbing. Water (data) flows continuously through pipes. You don't need to fill a bucket firstβyou process water as it flows, and you can connect multiple pipes together.
Data sources you can read from. The starting point of your data flow.
const readStream = fs.createReadStream('file.txt');
readStream.on('data', (chunk) => {
console.log(chunk);
});
Destinations you can write to. The endpoint where data lands.
const writeStream = fs.createWriteStream('out.txt');
writeStream.write('Hello');
writeStream.end('World');
Both readable and writable. Data flows both ways simultaneously.
// TCP sockets are duplex
const socket = net.createConnection(port);
socket.write('request'); // writable
socket.on('data', ...); // readable
Modify data as it passes through. The powerhouses of stream processing.
const { Transform } = require('stream');
const upperCase = new Transform({
transform(chunk, enc, cb) {
this.push(chunk.toString().toUpperCase());
cb();
}
});
Connect streams together to create powerful data processing pipelines. Data flows from source to destination, transformed along the way.
// Simple file copy
fs.createReadStream('input.txt')
.pipe(fs.createWriteStream('output.txt'));
const zlib = require('zlib');
// Read β Compress β Write
fs.createReadStream('file.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('file.txt.gz'));
const { Transform } = require('stream');
// Custom transforms
const upperCase = new Transform({
transform(chunk, enc, cb) {
this.push(chunk.toString().toUpperCase());
cb();
}
});
const removeSpaces = new Transform({
transform(chunk, enc, cb) {
this.push(chunk.toString().replace(/\s/g, ''));
cb();
}
});
// Chain them together
fs.createReadStream('data.txt')
.pipe(upperCase)
.pipe(removeSpaces)
.pipe(fs.createWriteStream('result.txt'));
.pipe() handles
backpressure automatically! It pauses the readable when the writable is overwhelmed,
preventing memory issues.
'data'
Chunk of data is available
'end'
No more data to read
'error'
Error occurred
'readable'
Data ready to be read
'close'
Stream closed
readStream.on('data', (chunk) => {
console.log(`Received ${chunk.length} bytes`);
});
readStream.on('end', () => {
console.log('Stream ended');
});
readStream.on('error', (err) => {
console.error('Error:', err);
});
'drain'
Safe to write more data
'finish'
All data has been flushed
'error'
Error occurred
'pipe'
Readable stream piped to this
'unpipe'
Stream unpiped
writeStream.on('finish', () => {
console.log('All data written');
});
writeStream.on('drain', () => {
console.log('Buffer cleared, resume writing');
});
Loads entire file into memory. Crashes with large files!
fs.readFile('large-video.mp4', (err, data) => {
if (err) throw err;
// π₯ Memory explosion!
// 2GB file = 2GB RAM used
fs.writeFile('copy.mp4', data, (err) => {
if (err) throw err;
console.log('Copied!');
});
});
Processes in small chunks. Constant memory usage regardless of file size.
const readStream = fs.createReadStream('large-video.mp4');
const writeStream = fs.createWriteStream('copy.mp4');
// β¨ Constant memory (~64KB buffer)
readStream.pipe(writeStream);
writeStream.on('finish', () => {
console.log('Copied!');
});
When the readable stream produces data faster than the writable stream can consume it, memory fills up and your application crashes. Here's how to handle it.
readStream.on('data', (chunk) => {
// write() returns false when buffer is full
const canContinue = writeStream.write(chunk);
if (!canContinue) {
console.log('Backpressure! Pausing...');
readStream.pause(); // Stop reading
}
});
writeStream.on('drain', () => {
console.log('Drained! Resuming...');
readStream.resume(); // Safe to continue
});
Without proper backpressure handling, your application will accumulate data in memory until it crashes with "JavaScript heap out of memory".
// β¨ .pipe() handles backpressure automatically!
// It pauses when needed, resumes when ready
fs.createReadStream('huge-file.txt')
.pipe(fs.createWriteStream('output.txt'));
// That's it. No manual pause/resume needed.
// The stream manages flow control for you.
Pauses reading when write buffer is full
Resumes when drain event fires
Memory stays constant regardless of speed difference
createReadStream()
Start reading
createWriteStream()
Start writing
.pipe()
Connect streams
.on('data')
Handle chunks
highWaterMark
Buffer size (default 64KB)
.pause()
Stop reading
.resume()
Continue reading
.on('error')
Handle errors