Node.js - Streaming 101: Duplex Streams Explained

Mar 6, 2025

Welcome back to Node.js Streaming 101! 🚀
If you haven’t checked out Part 1, I highly recommend reading it first—especially if you're new to streams in Node.js.

Today, we’re focusing on Duplex Streams, the true multi-taskers of the Node.js streaming world.

🔄 What Are Duplex Streams?

A Duplex Stream is a special type of stream that can be both readable and writable at the same time. Think of it like a walkie-talkie—you can send and receive data simultaneously.

Where Do We Use Duplex Streams?

Duplex Streams are everywhere:

  • Network sockets (TCP/WebSockets) 📡 – Sending and receiving data.
  • Data relay systems 🔄 – Processing and forwarding data in real time.
  • Serial port communication ⚡ – Interacting with hardware devices.

🏗 Creating a Simple Duplex Stream

Let’s create a custom Duplex Stream that buffers incoming data and allows it to be read later.

import { Duplex } from "stream";

class CustomDuplex extends Duplex {
  constructor(options) {
    super(options);
    this.data = []; // Temporary storage
  }

  _write(chunk, encoding, callback) {
    console.log(`Writing: ${chunk.toString()}`);
    this.data.push(chunk); // Store received data
    callback();
  }

  _read(size) {
    if (this.data.length > 0) {
      this.push(this.data.shift()); // Provide stored data when read is requested
    } else {
      this.push(null); // Signal end of stream
    }
  }
}

const duplexStream = new CustomDuplex();

duplexStream.write("Hello, ");
duplexStream.write("world!");
duplexStream.end(); // Signals that writing is complete

duplexStream.on("data", (chunk) => {
  console.log(`Reading: ${chunk.toString()}`);
});

🔍 What’s Happening?

  • We define a CustomDuplex class extending Duplex.
  • _write(chunk, encoding, callback): Stores incoming data.
  • _read(size): Reads the stored data when requested.
  • We write to the stream (write("Hello, ")), and later read from it (on("data")).

🧪 Expected Output

Writing: Hello,
Writing: world!
Reading: Hello,
Reading: world!

Boom! 🚀 You just built your first Duplex Stream!

⚡ Duplex Streams in Action: A TCP Echo Server

A real-world example of Duplex Streams is a TCP Echo Server, which receives data and immediately sends it back to the sender.

import { createServer } from "net";

const server = createServer((socket) => {
  console.log("Client connected");

  socket.on("data", (chunk) => {
    console.log(`Received: ${chunk.toString()}`);
    socket.write(`Echo: ${chunk.toString()}`); // Send data back
  });

  socket.on("end", () => {
    console.log("Client disconnected");
  });
});

server.listen(4000, () => {
  console.log("Echo server listening on port 4000...");
});

🧪 Test It!

  1. Run this script (node server.mjs).
  2. Open another terminal and use Netcat (nc):
    nc localhost 4000
  3. Type something (e.g., "Hello!") and hit Enter.
  4. The server echoes back "Echo: Hello!".

You just built a Duplex Stream-powered network application! 🚀

đź”— Using Duplex Streams with pipeline()

Let’s modify our Duplex Stream to work with pipeline() for better error handling.

import { Duplex, pipeline } from "stream/promises";

class EchoStream extends Duplex {
  _write(chunk, encoding, callback) {
    console.log(`Received: ${chunk.toString()}`);
    this.push(`Echo: ${chunk.toString()}`); // Push back modified data
    callback();
  }

  _read() {} // No need to implement, handled by push()
}

async function processDuplexStream() {
  const duplex = new EchoStream();
  try {
    await pipeline(
      process.stdin, // Input from terminal
      duplex, // Our custom Duplex Stream
      process.stdout // Output to terminal
    );
  } catch (err) {
    console.error("Pipeline failed:", err);
  }
}

processDuplexStream();

🧪 How to Use It

  1. Run this script (node duplex-pipeline.mjs).
  2. Type any text and press Enter.
  3. It echoes the text back.
Hello world
Echo: Hello world

🚀 Final Thoughts

Duplex Streams are incredibly powerful when you need bi-directional data flow.
Whether you’re handling network sockets, file forwarding, or real-time processing, they make data handling in Node.js fast, efficient, and flexible.

diogoaoliveira.com