jsguides

stream

The stream module is one of Node.js’s most powerful built-in modules. It provides APIs for handling streaming data efficiently, processing large amounts of data without loading everything into memory.

Overview

Streams are a fundamental concept in Node.js that allow you to read data piece by piece or write data incrementally. They are memory-efficient because they don’t require loading entire datasets into memory.

Types of Streams

Readable Streams

Readable streams produce data that can be consumed. Examples include:

  • fs.createReadStream() - reading files
  • process.stdin - standard input
  • HTTP responses on the client side
const fs = require('fs');

const readable = fs.createReadStream('file.txt', {
  encoding: 'utf8',
  highWaterMark: 1024  // chunk size in bytes
});

readable.on('data', (chunk) => {
  console.log('Received chunk:', chunk.length, 'bytes');
});

readable.on('end', () => {
  console.log('No more data');
});

readable.on('error', (err) => {
  console.error('Error:', err);
});

A readable stream produces chunks that you consume through event listeners. But data rarely stops at consumption — you usually need to send it somewhere else. That is where writable streams come in: they accept chunks and write them to a destination. The example below writes to a file, but the same pattern applies to network sockets, transform pipelines, or any writable target.

Writable Streams

Writable streams receive and process data. Examples include:

  • fs.createWriteStream() - writing files
  • process.stdout - standard output
  • HTTP requests on the client side
const fs = require('fs');

const writable = fs.createWriteStream('output.txt');

writable.write('Hello, ');
writable.write('World!\n');
writable.end('Done writing');

writable.on('finish', () => {
  console.log('All data written');
});

writable.on('error', (err) => {
  console.error('Error:', err);
});

Readable and writable streams each handle one direction of data flow independently. Transform streams combine both — they sit in the middle, accepting input on the writable side and producing output on the readable side after applying some transformation. That makes them the building block for data pipelines that need to modify chunks in transit, like compression, encryption, or character encoding.

Transform Streams

Transform streams modify data as it passes through. They are both readable and writable.

const { Transform } = require('stream');

const upperCase = new Transform({
  transform(chunk, encoding, callback) {
    this.push(chunk.toString().toUpperCase());
    callback();
  }
});

upperCase.on('data', (chunk) => {
  console.log(chunk.toString()); // HELLO WORLD
});

upperCase.write('hello ');
upperCase.write('world');
upperCase.end();

A transform stream connects its readable and writable sides in a defined processing pipeline — what goes in gets transformed and comes out. A duplex stream is different: the readable and writable channels are independent. Data written to one side has no automatic relationship to data read from the other. That independence makes duplex streams a better model for bidirectional communication, such as a TCP socket where each direction carries its own stream of messages.

Duplex Streams

Duplex streams implement both readable and writable interfaces independently. A good example is a TCP socket.

const { Duplex } = require('stream');

const duplex = new Duplex({
  read(size) {
    // Push data to be read
    this.push('Hello from duplex ');
    this.push(null); // Signal end of stream
  },
  write(chunk, encoding, callback) {
    // Process written data
    console.log('Received:', chunk.toString());
    callback();
  }
});

duplex.on('data', (chunk) => {
  console.log('Read:', chunk.toString());
});

duplex.write('Writing to duplex ');
duplex.end();

Understanding the four stream types is step one. The practical need is connecting them together — piping a readable into a writable, or chaining several transforms. Node.js provides stream.pipeline() as the recommended way to do this, because it handles error propagation and cleanup that manual .pipe() chains often miss.

Pipeline

The stream.pipeline() function pipes between streams asynchronously, handling errors and cleanup automatically.

const fs = require('fs');
const { pipeline } = require('stream/promises');

async function copyFile(source, destination) {
  const readStream = fs.createReadStream(source);
  const writeStream = fs.createWriteStream(destination);

  await pipeline(readStream, writeStream);
  console.log('Pipeline succeeded');
}

copyFile('input.txt', 'output.txt').catch((err) => {
  console.error('Pipeline failed:', err);
});

pipeline() orchestrates the flow between streams, but sometimes you only need to know when a single stream is done. stream.finished() gives you a promise that resolves when a stream emits its end or finish event, which is cleaner than setting up listeners by hand. It is useful for one-off checks like waiting for a file read to complete before starting the next step in a script.

Finished

The stream.finished() function waits for the stream to finish.

const fs = require('fs');
const { finished } = require('stream/promises');

const readStream = fs.createReadStream('file.txt');

await finished(readStream);
console.log('Stream finished');

finished() tells you when a stream ends, but it does not help you manage the speed of data passing through. That is where backpressure comes in. A fast producer can overwhelm a slow consumer, filling internal buffers and wasting memory. The pattern below shows how to pause writing when the stream signals it is full and resume only after it drains — a manual form of the coordination that pipeline() handles automatically.

Backpressure

When writing to a stream, you must handle backpressure. If the internal buffer is full, pause writing until it drains.

const fs = require('fs');

const writable = fs.createWriteStream('output.txt');

function writeData(data) {
  const canContinue = writable.write(data);
  if (!canContinue) {
    writable.once('drain', () => {
      writeData(moreData);
    });
  }
}

All the examples so far work with strings or buffers — the default mode for Node.js streams. For structured data, object mode flips the model: each chunk is a JavaScript object rather than raw bytes. That makes streams useful beyond file I/O, letting you pipe records through the same pipeline patterns you would use for binary data.

Object Mode

Streams can work with JavaScript objects instead of strings or buffers by enabling object mode.

const { Readable } = require('stream');

const objectStream = new Readable({
  objectMode: true,
  read() {
    this.push({ name: 'Alice', age: 30 });
    this.push({ name: 'Bob', age: 25 });
    this.push(null); // End stream
  }
});

objectStream.on('data', (obj) => {
  console.log(obj); // { name: 'Alice', age: 30 }
});

Why streams matter

Streams let Node.js process data incrementally instead of loading everything at once. That is the big win: files, network responses, and generated output can move through the system in chunks, which keeps memory use predictable and lets work begin before the full payload is available.

That model also fits real-world workflows better than a single giant buffer. You can transform, validate, and forward chunks as they arrive. For large files or slow connections, that means the program stays responsive while still handling the complete data stream.

Backpressure and Pipelines

The most important operational detail is backpressure. A writable stream can only accept data so quickly, and stream.pipeline() helps coordinate the flow so fast producers do not overwhelm slow consumers. If you hand-roll stream logic, make sure you respect write() return values and wait for drain when needed.

In most application code, pipeline() is the safest default because it centralizes error handling and cleanup. Use lower-level stream APIs when you need custom control, but let the built-in helpers do the heavy lifting whenever possible.

See Also

  • Fs — File system operations that return streams
  • Buffer — Buffer for binary data
  • Events — Event emitter pattern