Node.js Stream Internals: Data Production, Consumption, and Back‑pressure
The article explains Node.js stream internals, showing how readable, writable, and transform streams manage data production and consumption, the roles of _read, read, push, and doRead, the two operating modes, and how pipe implements back‑pressure via high‑water marks to ensure memory‑safe, efficient processing.
When building complex systems, it is common to split them into independent components that communicate through well‑defined interfaces, similar to how a shell connects commands with a pipe that streams text.
Node.js provides a built‑in Stream module that implements this idea; individual parts are linked via .pipe().
This article dives into the internal mechanics of Node.js streams, focusing on how streaming data is processed and how the back‑pressure mechanism works.
Data Production and Consumption
The stream acts as a medium between a data source (upstream) and a consumer (downstream). A simple fs.readFile example shows that reading a large file into memory fails because the resulting Buffer is too big:
const fs = require('fs')
fs.readFile(file, function (err, body) { console.log(body)
console.log(body.toString())
})Using a readable stream solves the problem:
const fs = require('fs')
fs.createReadStream(file).pipe(process.stdout) fs.createReadStreamcreates a readable stream that connects the file (source) to process.stdout (sink). The stream repeatedly calls fs.read to fetch chunks of data.
Readable Stream Workflow
A Readable object is created with new Readable() or require('stream').Readable. Implementing the internal _read method links the stream to a low‑level data source. When the consumer calls read(n), the stream either returns data from its internal buffer or triggers _read to pull more data.
The stream pushes data to the consumer via the push method. If the stream is in flowing mode ( state.flowing === true) and the buffer is empty, it emits a data event immediately; otherwise the data is stored in the buffer and a readable event is emitted when needed.
Key Internal Functions
read – decides how many bytes to return based on the current state.
push – called by _read to deliver data to the stream; may emit data directly or buffer the chunk.
doRead – determines whether a new read from the underlying source is required.
var doRead = state.needReadable || (state.length === 0 || state.length - n < state.highWaterMark)
if (state.ended || state.reading) doRead = false
if (doRead) {
state.reading = true
state.sync = true
if (state.length === 0) state.needReadable = true
this._read(state.highWaterMark)
state.sync = false
}howMuchToRead – calculates the actual number of bytes that will be returned by read(n) based on several conditions (empty buffer, object mode, end of stream, etc.).
Consumption Modes
Streams can operate in two modes:
Paused mode – the consumer must explicitly call read() or listen to the readable event.
Flowing mode – data is emitted automatically via the data event.
In paused mode, calling read() may trigger _read if the buffer is insufficient. If _read pushes data asynchronously, read() can return null, and the consumer must wait for the readable event.
const Readable = require('stream').Readable
const dataSource = ['a','b','c']
const readable = Readable()
readable._read = function () {
if (dataSource.length) this.push(dataSource.shift())
else this.push(null)
}
readable.pause()
readable.on('readable', () => {
let chunk
while (null !== (chunk = readable.read())) process.stdout.write(chunk)
})In flowing mode, attaching a data listener or calling pipe() automatically switches the stream to flowing.
Back‑pressure and Pipe
The pipe() method connects a readable stream to a writable stream, propagating back‑pressure: when writable.write() returns false, the upstream readable is paused; when the writable emits drain, the readable resumes.
readable.on('data', data => {
if (false === writable.write(data)) readable.pause()
})
writable.on('drain', () => readable.resume())Example showing how high‑water marks limit production:
const stream = require('stream')
let c = 0
const readable = stream.Readable({
highWaterMark: 2,
read() {
process.nextTick(() => {
const data = c < 6 ? String.fromCharCode(c + 65) : null
console.log('push', ++c, data)
this.push(data)
})
}
})
const writable = stream.Writable({
highWaterMark: 2,
write(chunk, enc, next) { console.log('write', chunk) }
})
readable.pipe(writable)The output demonstrates that only four chunks (A‑D) are produced because the writable’s buffer fills, causing the readable to pause.
Transform Streams
When a Transform stream is used as the downstream, it has both writable and readable buffers. Back‑pressure can stop upstream production if either buffer reaches its high‑water mark.
const transform = stream.Transform({
highWaterMark: 2,
transform(buf, enc, next) { console.log('transform', buf); next(null, buf) }
})
readable.pipe(transform)Only a limited number of chunks are produced until the transform’s internal buffers are full.
Summary
Node.js streams provide a powerful abstraction for handling large or continuous data flows. Understanding the internal state machine ( state.flowing, state.length, state.highWaterMark), the roles of read, push, _read, and the back‑pressure mechanism is essential for building efficient, memory‑safe applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Meituan Technology Team
Over 10,000 engineers powering China’s leading lifestyle services e‑commerce platform. Supporting hundreds of millions of consumers, millions of merchants across 2,000+ industries. This is the public channel for the tech teams behind Meituan, Dianping, Meituan Waimai, Meituan Select, and related services.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
