A Readable is a source with an internal queue. It starts paused: nothing is read until you ask. A data listener, resume(), a pipe or a for await loop switches it to flowing mode, where chunks arrive as fast as the destination accepts them; read() and pause() keep you in charge of the pace.
The queue size is the highWaterMark: how many bytes (or objects, in objectMode) Node buffers ahead before it stops reading. It is a target, not a limit — push() always accepts a chunk, returning false to say stop producing.
const file = createReadStream('orders.csv', { highWaterMark: 64 * 1024 });
let chunks = 0, bytes = 0;
file.on('data', (chunk) => { chunks++; bytes += chunk.length; });
file.on('end', () => console.log(`${chunks} chunks, ${bytes} bytes`));
await new Promise((resolve) => file.on('close', resolve));
const counter = new Readable({ objectMode: true, read() {
this.n = (this.n ?? 0) + 1;
this.push(this.n > 3 ? null : { tick: this.n }); // null ends the stream
} });
for await (const value of counter) console.log(value);1736 chunks, 113726870 bytes
{ tick: 1 }
{ tick: 2 }
{ tick: 3 }getDefaultHighWaterMark() reports the platform default: 16 KB on Windows, 64 KB elsewhere, 16 for object streams. The 64 KB highWaterMark above turned a 108 MB file into 1736 chunks instead of about 6940 at the Windows default. Raising it trades memory for fewer reads, up to where the device saturates.
The lifecycle events are data, end (all data consumed), error and close (resources released). end never fires if you stop consuming, and error does not imply end, so cleanup belongs on close — or to pipeline (Piping Safely with pipeline). Readable.from(iterable) wraps any iterable, generators included.