Events, streams and buffers

EventEmitter patterns, readable, writable and transform streams, backpressure, and the byte-level rules of Buffer.

EventEmitter

Much of Node's API is an EventEmitter: objects that announce facts and let any number of listeners react. Emitting is synchronous — every listener runs before emit returns.

import { EventEmitter } from "node:events";

const bus = new EventEmitter();

bus.on("order.paid", (order) => console.log("ship", order.id));
bus.once("ready", () => console.log("first time only"));
bus.on("error", (err) => console.error("bus failed", err));

bus.emit("order.paid", { id: 1042 });   // listeners run right here
bus.listenerCount("order.paid");        // 1
bus.removeListener("order.paid", handler);
bus.removeAllListeners();
  • on returns the emitter, which is why calls chain.
  • An error event with no listener is rethrown as an uncaught exception and takes the process down — always attach one on emitters you own.
  • once, off and removeAllListeners keep long-lived emitters from accumulating listeners forever.
  • Ten listeners on one event triggers a warning; raise it deliberately with setMaxListeners rather than ignoring it.

Streams and pipeline

A stream is an emitter of chunks with backpressure built in. Instead of holding a whole file or response in memory, you transform it piece by piece with roughly constant memory.

import { createReadStream, createWriteStream } from "node:fs";
import { Transform } from "node:stream";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";

const upper = new Transform({
  transform(chunk, enc, cb) {
    cb(null, chunk.toString().toUpperCase());
  },
});

await pipeline(
  createReadStream("access.log"),
  upper,
  createGzip(),
  createWriteStream("access.log.gz")
);
KindExampleYou consume it with
Readablefs.createReadStream, process.stdin, an HTTP requestfor await, .on("data"), .read()
Writablefs.createWriteStream, an HTTP response.write(), then await "drain"
Duplexnet.Socket, a TCP connectionBoth directions independently
Transformzlib.createGzip, crypto.createCipherivWrite in, read out, in order
PassThroughA tap for inspection or testingForwards every chunk untouched
💡
Use stream/promises pipeline rather than a chain of .pipe() calls. pipe forwards data but not errors, so a failure upstream can leave the destination open and the promise never settling.

Buffers, encoding and backpressure

const buf = Buffer.from("hello", "utf8");

buf.length;                       // 5 — bytes, not characters
buf.toString("base64");           // "aGVsbG8="
Buffer.byteLength("héllo");  // 6 — the accent is two bytes
Buffer.concat([buf, Buffer.from("!")]);   // one allocation, no string round-trip

// honour backpressure: write() returns false when the buffer is full
import { once } from "node:events";
if (!out.write(chunk)) {
  await once(out, "drain");
}
  • str.length counts UTF-16 code units while buf.length counts bytes; assuming they match will corrupt non-ASCII text.
  • Buffer.alloc(n) zero-fills; Buffer.allocUnsafe(n) skips the fill and can expose whatever the previous allocation held, so only use it when you overwrite every byte before reading.
  • Never slice a UTF-8 buffer in the middle of a character, and prefer StringDecoder when chunk boundaries may split one.
  • Setting setEncoding("utf8") on a readable hides bytes but still splits characters across chunks in edge cases.
  • A stream that never drains is the usual cause of a process that slowly grows in memory while doing almost nothing.

FAQ

When is a stream overkill?
When the payload is small and truly needed in full — a configuration file, a JSON body under a megabyte, a single row. readFile is simpler and faster there. Streams pay off above a few megabytes or when you want the first byte to arrive before the last is generated.
Why does my transform stream output arrive in odd chunks?
Streams deliver whatever size the source produced, not logical lines. Add a splitter, or accumulate a partial tail in the transform and emit it when the next chunk completes a record.

Modules, ESM and CommonJS Worker threads, child processes and clustering

Last refreshed 2026-09-18.