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();onreturns the emitter, which is why calls chain.- An
errorevent with no listener is rethrown as an uncaught exception and takes the process down — always attach one on emitters you own. once,offandremoveAllListenerskeep long-lived emitters from accumulating listeners forever.- Ten listeners on one event triggers a warning; raise it deliberately with
setMaxListenersrather 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")
);| Kind | Example | You consume it with |
|---|---|---|
| Readable | fs.createReadStream, process.stdin, an HTTP request | for await, .on("data"), .read() |
| Writable | fs.createWriteStream, an HTTP response | .write(), then await "drain" |
| Duplex | net.Socket, a TCP connection | Both directions independently |
| Transform | zlib.createGzip, crypto.createCipheriv | Write in, read out, in order |
| PassThrough | A tap for inspection or testing | Forwards 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.lengthcounts UTF-16 code units whilebuf.lengthcounts 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
StringDecoderwhen 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.
Related
Modules, ESM and CommonJS Worker threads, child processes and clustering
Last refreshed 2026-09-18.