One document, two ends of the same stream, plus the type question the write side leaves open. The read side reproduces the original four questions; the write side asks the same four of the other end.

stream.Readable, IncomingMessage, and the two consumption interfaces

Four questions, answered in the order they were asked. Each answer is reproduced exactly as it was given. Underneath each one, a panel goes back over the same ground with the mechanism spelled out — what the keyword does at the position it sits in, what the real object graph looks like, and where the concise version is standing in for something more specific.

Every diagram, table and code pane on this page uses one colour per concept.

producer — network, kernel, socket Readable — state, buffer, flow control EventEmitter — push interface async iterator — pull interface HTTP metadata promise, suspension, resumption

Click any outlined code token for a definition. Every diagram is a cell-for-cell transcription of the original — same branches, same tiers, same labels. Checked against Node 24 LTS and 26 current; every version-sensitive figure names the release that changed it.

first questionwhat shape is stream.Readable?

The shape of a Readable

In Node.js, stream.Readable is best understood as an object implementing the async, pull-oriented side of a stream.

At the protocol level, think:

Readable │ ├── state │ ├── buffer │ ├── ended? │ ├── errored? │ └── flowing / paused │ ├── methods │ ├── read() │ ├── push() │ ├── pipe() │ ├── pause() │ ├── resume() │ └── destroy() │ ├── events │ ├── data │ ├── end │ ├── error │ └── readable │ └── protocols ├── AsyncIterable │ └── [Symbol.asyncIterator]() └── EventEmitterReadablestatebufferended?errored?flowing / pausedmethodsread()push()pipe()pause()resume()destroy()eventsdataenderrorreadableprotocolsAsyncIterable[Symbol.asyncIterator]()EventEmitter

Where each of those four branches physically lives

The tree mixes four different kinds of thing, and they sit in four different places in the object graph. That matters the moment you start reasoning about inheritance, because only one of the four is copied per instance.

BranchWhere it actually livesPer instance or shared?
stateOne object hanging off the instance at _readableStatePer instance — every stream has its own buffer and flags
methodsData properties on Readable.prototypeShared — one function object, found by prototype lookup
eventsNothing until you subscribe; then handler arrays on this._eventsPer instance
protocolsA method keyed by a well-known symbol on the prototypeShared

So "shape" is not one flat record. It's own state on the instance plus behaviour reached by walking the prototype chain. The next question in this sequence is entirely about that distinction.

The events branch is not a fixed list either

The four names shown are the ones a consumer subscribes to, but a Readable also emits 'close', 'pause' and 'resume'. Of the four listed, 'readable' is the one that carries the most weight later on: it is the event the async-iterator interface is built on, and it is the only one of the four that never appears again in the rest of this material.

read() and push() are not the same kind of method

They sit next to each other in the tree, which reads as though a consumer might call either. Only one of them is yours.

MethodWho calls itWhat it means
read()the consumerTake up to n bytes out of the buffer, now
pipe()the consumerWire this readable end into a writable end
pause() / resume()the consumerToggle flowing mode
destroy()the consumerTear the stream down, release the resource
push()the implementerFeed a chunk into the buffer from the underlying resource
_read()the implementerThe hook Node calls to say "go get more"

On an incoming HTTP request you never call push(). Node's HTTP layer calls it for you every time the parser finishes extracting a piece of the body. Putting it in the consumer's list is the single easiest way to end up confused about which side of the stream you are standing on.

The important distinction

A Readable is not simply an async iterator.

Conceptually:

Readable │ ┌───────────┼────────────┐ │ │ │ buffer events async iterator │ │ │ read() 'data' next() │ │ │ └───────────┼────────────┘ │ consumerReadablebuffereventsasync iteratorread()'data'next()consumer

The underlying stream has state + buffering + production mechanics.

Its async-iterator interface is just one way to consume that stream:

for await (const chunk of readable) {
    console.log(chunk);
}

Conceptually, that loop is consuming something like:

const iterator = readable[Symbol.asyncIterator]();

await iterator.next();
// { value: chunk, done: false }

await iterator.next();
// { value: chunk, done: false }

await iterator.next();
// { value: undefined, done: true }

So connecting this to the model you've been building:

LOOP ↓ ITERATOR ↓ STREAMLOOPITERATORSTREAM

A Readable is the stream layer.

It can expose an async iterator view of itself, but the stream is richer than that iterator.

What each of the three layers actually owns

The three-word stack is worth expanding, because each layer owns something the layer above it cannot see.

LayerOwnsCannot see
LOOPThe decision to ask for another value, and the body that runs per valueWhether the value was buffered or waited for
ITERATORThe { value, done } protocol and one pending request at a timeHow many chunks are queued behind the one it returned
STREAMBuffer, watermark, flow mode, error and end state, the underlying resourceNothing below it — it is the layer that talks to the producer

"The stream is richer than that iterator" is precisely this: the iterator protocol has exactly two verbs, next() and (optionally) return(). The stream underneath has a buffer with a size, a watermark that governs whether to keep reading from the resource, three flow states, and a destroy path. None of that is expressible in { value, done }.

"async, pull-oriented side of a stream"

Two things are being compressed here, and separating them prevents a contradiction later on.

  • Which side. Readable is the readable end — the side a consumer takes bytes out of. Its opposite number is Writable, the end you put bytes into. That framing is exact.
  • Pull versus push. This is not a property of the class. A Readable supports both, and which one you get depends on how you consume it: attach a 'data' handler and the stream pushes at you; call read() or iterate it and you pull. The same object, two modes.

Calling the class "the pull side" collides with the push/pull section further down this page, where 'data' is correctly identified as push. The stable statement is: Readable is the consumer-facing end of a stream, and it exposes both a push interface and a pull interface over one buffer.

for await, token by token

for await (const chunk of readable) { … }
  • for await — legal only inside an async function or a module top level, because it needs somewhere to suspend to.
  • of — makes this an iteration over a sequence protocol, not over an object's keys. The engine calls a method on readable rather than reading its properties.
  • const chunk — a fresh binding created per iteration, in a new declarative environment record for each pass of the loop body.
  • [Symbol.asyncIterator] — the method the engine looks up first. If it is absent, the engine falls back to [Symbol.iterator] and wraps it. A Readable has the async one, so the fallback never fires here.

One step the concise desugaring leaves out: if the loop body throws, or you break, or you return, the engine calls iterator.return() on the way out. On a Readable that call destroys the stream. This is why breaking out of a for await over an HTTP request mid-body leaves the request destroyed rather than merely un-iterated — a behaviour with no equivalent in the on('data') version.

The deepest mental model

For a normal async iterator:

consumer │ │ next() ↓ iterator │ │ produces value ↓ consumerconsumernext()iteratorproduces valueconsumer

For a Readable:

producer │ ↓ ┌───────────────┐ │ Readable │ │ │ │ buffer │ │ ↓ │ │ chunks │ └───────┬───────┘ │ ┌─────┴─────┐ ↓ ↓ events async iterator │ │ ↓ ↓ consumer consumerproducerReadablebufferchunkseventsasync iteratorconsumerconsumer

And backpressure is what makes it fundamentally stream-like:

producer │ ↓ [ Readable buffer ] │ ↓ consumer ↑ │ backpressureproducer[ Readable buffer ]consumerbackpressure

What that upward arrow is made of

"Backpressure" is drawn as an arrow pointing back at the producer, which is the right direction but leaves out every link in the chain. On an incoming HTTP request, no message travels upward. What happens is a sequence of things that stop.

_readableState buffer > highWaterMark _read() not called Node stops reading socket paused no more recv() calls kernel recv queue fills up, nothing drains TCP window shrinks advertised in every ACK sender stops sending Nothing is sent upstream. Each link is something that stops happening.

The consequence worth holding on to: backpressure on a read path is the absence of recv() calls, not a signal. It is enforced by TCP itself, several layers below JavaScript. That is also why it only works if your consumer genuinely stops asking — which the two consumption interfaces do not handle the same way, as the last stage of this page shows.

So if you're asking "what shape is stream.Readable?", the concise answer is:

Readable is a stateful stream object that provides multiple consumption interfaces, including an async-iterator interface.

That's why Readable sits above the iterator abstraction rather than being equivalent to it.

The state, with its real field names

Every flag in the "state" branch is a property on one object. Reading it in a debugger is the fastest way to make all of this concrete.

Field on _readableStateTypeWhat it governs
bufferBufferListLinked list of chunks not yet handed to a consumer
lengthnumberTotal bytes buffered — the number compared against the watermark
highWaterMarknumberAdvisory ceiling. Exceeding it stops Node asking for more; it does not cap the buffer
flowingnull | false | trueThree states, not two
endedbooleanThe producer has signalled end-of-data
endEmittedbooleanThe 'end' event has actually fired — later than ended
destroyedbooleanUnderlying resource released
errorednull | ErrorHolds the error rather than just a flag
objectModebooleanIf true, length counts items instead of bytes

flowing has three values, and the third one is the default

The tree shows "flowing / paused", which reads as a boolean. It is not.

ValueMeaningHow you get there
nullNo consumer has committed yet. Data is not read from the resource and not buffered ahead.The initial value of every new Readable
trueFlowing. Chunks are pushed out as 'data' events as fast as they arrive.Adding a 'data' listener, calling resume(), or pipe()
falsePaused. Chunks accumulate in the buffer until someone calls read().Calling pause(), or adding a 'readable' listener

The null state is why a request whose body you never consume simply sits there rather than filling memory, and it is why an unconsumed request can hold a connection open until it times out.

second questionso if NodeReq extends stream.Readable, does that mean it gets everything Readable has plus its own request-specific state — is inheritance just extending a shape?

Inheritance is a chain, not a copy

Exactly. If we write:

class NodeReq extends stream.Readable {
    // ...
}

then NodeReq has the shape of a Readable plus additional request-specific state and behavior.

Think inheritance = extending a shape.

NodeReq │ ├── everything from Readable │ │ ├── read() │ ├── push() │ ├── pipe() │ ├── pause() │ ├── resume() │ ├── destroy() │ ├── [Symbol.asyncIterator]() │ ├── events: data / end / error / readable │ └── internal readable state │ └── HTTP request-specific stuff ├── method ├── url ├── headers ├── httpVersion ├── socket ├── trailers └── ...NodeReqeverything from Readableread()push()pipe()pause()resume()destroy()[Symbol.asyncIterator]()events: data / end / error / readableinternal readable stateHTTP request-specific stuffmethodurlheadershttpVersionsockettrailers...

What extends does at the position it sits in

The keyword establishes two links, not one. Most explanations mention only the first.

LinkSet toWhat it makes possible
NodeReq.prototype.[[Prototype]]Readable.prototypeInstance lookup finds read, pipe, on
NodeReq.[[Prototype]]Readable (the constructor itself)Static members and Readable.from are reachable as NodeReq.from

Two more consequences that only appear once you write a real subclass body:

  • constructor — if you declare one, you must call super(options) before touching this. In a derived class the binding for this starts out uninitialised in the function environment record; super() is the thing that runs the parent constructor and populates it. Reading this first throws a ReferenceError, not undefined.
  • super(options) — for a Readable this is where _readableState gets constructed, and where options.highWaterMark, objectMode and encoding are read. Skip it and every method you inherited will throw, because they all reach for state that was never built.

If you omit the constructor entirely — as the // ... body does — you implicitly get constructor(...args) { super(...args); }, which is why the class above is already a working Readable.

So if you're looking at Node's HTTP request:

const req = http.createServer((req, res) => {
    // req is an IncomingMessage
});

the conceptual type relationship is:

IncomingMessage │ │ extends ↓ Readable │ │ extends ↓ EventEmitterIncomingMessageextendsReadableextendsEventEmitter

Therefore:

req.on(...)

works because Readable ultimately provides EventEmitter behavior.

And:

for await (const chunk of req) {
    ...
}

works because Readable provides the async-iteration interface.

But:

req.method
req.url
req.headers

are not stream concepts. They're added by IncomingMessage.

That snippet binds the server, not the request

const req = http.createServer((req, res) => { … });

http.createServer returns a Server. So the outer req is a server object, and the inner req parameter shadows it inside the callback — two different values under one name, in two nested environment records. The line that gives you an IncomingMessage is:

const server = http.createServer((req, res) => {
    // req is an IncomingMessage, res is a ServerResponse
});
server.listen(8080);

The chain has four links, not three

Readable does not inherit from EventEmitter directly. It inherits through the original Stream class, which is the legacy base that also supplies pipe. Every lookup that ends at an emitter method passes through it.

req own props: method, url, headers… IncomingMessage.prototype Readable.prototype Stream.prototype EventEmitter.prototype Object.prototype → null httpVersion, setTimeout, _addHeaderLine read, push, pause, resume, [Symbol.asyncIterator] pipe on, once, emit, removeListener req.read() walks two links, finds it on Readable req.method own property — no walk at all

One further wrinkle worth knowing before you go reading Node's source: IncomingMessage is not written with class … extends. It is a function constructor whose prototype is wired up manually:

function IncomingMessage(socket) {
  // …
  Readable.call(this, streamOptions);
}
ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype);
ObjectSetPrototypeOf(IncomingMessage, Readable);

Those two ObjectSetPrototypeOf calls are exactly the two links extends would have created, written by hand. Readable.call(this, …) is what super() would have done. The resulting shape is identical — so the class NodeReq extends stream.Readable model is a faithful description of the relationship, just not of the syntax.

The key first-principles distinction

Don't visualize this as:

NodeReq = { method, url, headers, read, ... }

as though someone simply copied properties into one object.

Instead:

IncomingMessage │ prototype chain ↓ Readable │ ↓ EventEmitter │ ↓ Object.prototypeIncomingMessageprototype chainReadableEventEmitterObject.prototype

So when JavaScript evaluates:

req.read()

it effectively searches:

req ↓ IncomingMessage.prototype ↓ Readable.prototype ↓ ...reqIncomingMessage.prototypeReadable.prototype...

until it finds read.

Whereas:

req.method

is request-specific state associated with the IncomingMessage.

The search, stated exactly

The engine performs [[Get]] on req with the key "read":

  1. Look for an own property named read on req. Not found.
  2. Follow req.[[Prototype]] and repeat. Not found on IncomingMessage.prototype.
  3. Follow again. Found on Readable.prototype. Return that function object.
  4. The call then runs with this bound to req — because this is decided by the call site, not by where the method was found. This is the mechanism by which one shared function operates on per-instance state.

That last point is the whole reason the design works. There is exactly one read function in the process, and it reaches this._readableState to find which stream it is reading.

Where each property on a request actually resolves

ExpressionResolves atKind
req.methodown property on the instanceString, assigned by the parser
req.urlown property on the instancePath and query only — never the scheme or host
req.headersown property on the instanceLazily built object, keys lower-cased
req.socketown property on the instanceThe underlying net.Socket, shared by every request on that connection
req.httpVersionown property on the instanceAssembled from major and minor
req.readReadable.prototypeShared function
req.onEventEmitter.prototypeShared function, three links up
req.pipeStream.prototypeShared function — the one method that lives on the legacy base

When the metadata is populated

"Request-specific state" arrives at a specific moment, and it is earlier than the body. Node's HTTP parser signals headers-complete first; only then is your createServer callback invoked. So on the very first line of your handler:

  • req.method, req.url and req.headers are already final.
  • The body has generally not arrived. Zero, one or several chunks may be sitting in the socket, but the stream has not been consumed.
  • req.trailers is an empty object and stays that way until 'end' — trailers are sent after the body, which is the entire reason they exist.

That split — metadata ready synchronously, body available only over time — is what forces the object to be both a record and a stream at once.

And this connects directly to your iterator model

IncomingMessage is a stream that happens to expose an async iterator:

IncomingMessage │ ├── HTTP request identity │ method │ url │ headers │ └── Readable │ ├── buffering ├── flow control ├── events │ └── async iterator │ └── next()IncomingMessageHTTP request identitymethodurlheadersReadablebufferingflow controleventsasync iteratornext()

So:

NodeReq isn't an iterator. It's a Readable stream object whose stream contains the incoming HTTP request body, with HTTP-request metadata layered on top.

"Happens to expose" is doing real work in that sentence

Being iterable is a property the object acquired by inheritance, not something the HTTP layer arranged. IncomingMessage contains no iteration code whatsoever. The method is defined once, on Readable.prototype, keyed by Symbol.asyncIterator, and every readable stream in the process picks it up for free — file reads, sockets, decompression streams, request bodies.

The practical test of whether you have internalised this: req and a fs read stream are iterated by the exact same function. Nothing in that function knows about HTTP.

third question · 3:24 AMdemonstrate how this works in a way i can understand the async iterator/generator and eventEmitter in this system

One object, three protocols

Yes. The easiest way to understand this is to stop thinking of IncomingMessage as one thing.

It is one object that participates in three different protocols:

HTTP Request │ ▼ ┌───────────────────┐ │ IncomingMessage │ │ │ │ HTTP metadata │ │ + │ │ Readable stream │ └─────────┬─────────┘ │ ┌───────────────┼────────────────┐ │ │ │ ▼ ▼ ▼ EventEmitter Async Iterator Readable interface interface mechanicsHTTP RequestIncomingMessageHTTP metadata+Readable streamEventEmitterAsync IteratorReadableinterfaceinterfacemechanics

Let's build a tiny version from scratch.

1. Start with the problem

Imagine a browser sends:

POST /upload

hello world

The network does not give Node the entire "hello world" string at once.

Maybe it arrives:

time → t1 t2 t3 "hel" "lo " "world"timet1t2t3"hel""lo ""world"

So Node needs an object that can represent:

"There is an HTTP request, and its body is arriving over time."

That's what IncomingMessage does.

Why the split is guaranteed, not merely possible

"Maybe it arrives" understates it. TCP offers a byte stream with no message boundaries. There is no framing in the protocol that would let the kernel hand your process a complete HTTP body as a unit, and several independent mechanisms actively chop it up:

  • Path MTU. A segment carries roughly 1460 bytes of payload on a standard Ethernet path. A 10 MB body is thousands of segments before it ever reaches the receiver.
  • Kernel receive buffer. Bytes accumulate in the socket's receive queue. Your process gets whatever has arrived at the instant it calls recv() — a partial segment, several segments coalesced, anything.
  • Congestion and flow control. The sender's window opens and closes over the life of the transfer, so arrival is bursty by design.
  • The HTTP parser. Even a single recv() may contain the tail of the headers plus the head of the body, so what reaches the stream is a slice of a slice.

The consequence for your code: a chunk boundary carries no meaning. A multi-byte UTF-8 character can be split across two chunks; a JSON document certainly can. Any code that calls chunk.toString() per chunk and concatenates the strings is already broken for non-ASCII input, and the bug only shows up under a particular chunking.

2. The IncomingMessage shape

Conceptually:

class IncomingMessage extends Readable {

    method = "POST";
    url = "/upload";
    headers = {...};

}

Because it extends Readable, it inherits stream machinery.

So:

req.method
req.url
req.headers

come from the HTTP layer.

While:

req.on(...)
req.read()
req.pipe(...)
req[Symbol.asyncIterator]()

come from the stream layer.

Class fields, at the position they sit in

method = "POST" written directly in a class body is a public instance field. Three things follow from that placement:

  • It creates an own property on every instance, not a shared one on the prototype. Which is correct here — every request has its own method.
  • It is installed after super() returns. So the field initialisers run once the Readable half of the object already exists; you cannot read this.method from inside a parent constructor.
  • The initialiser runs per construction, so headers = {} creates a fresh object each time. Had it been assigned to the prototype, every request in the process would share one headers object.

In real Node the values are not literals in a class body — they are assigned by the parser as it reads the request line. The class-field version is the right shape with the timing removed.

3. Now add EventEmitter

A Readable is also an EventEmitter.

So conceptually:

IncomingMessage │ ↓ Readable │ ↓ EventEmitterIncomingMessageReadableEventEmitter

That gives us:

req.on("data", callback);
req.on("end", callback);
req.on("error", callback);

Think of EventEmitter as:

"When something happens, call these functions."

For example:

req.on("data", chunk => {
    console.log("received:", chunk);
});

You're effectively saying:

EventEmitter:

"Whenever a data event happens, call THIS function."

There is less machinery here than the name suggests

An EventEmitter is a map from event name to a function or an array of functions, plus a loop. Stripped to essentials:

on(name, fn) {
  (this._events[name] ??= []).push(fn);
  return this;
}

emit(name, ...args) {
  const list = this._events[name];
  if (!list) return false;
  for (const fn of [...list]) fn.apply(this, args);
  return true;
}

Four properties follow directly from that loop, and all four surprise people:

  • emit is fully synchronous. It is a for loop. By the time emit returns, every listener has already run to completion. Nothing is queued, and there is no event loop involvement at all.
  • The list is copied before iterating. A listener that removes itself, or adds another, does not disturb the pass in progress.
  • Order is registration order. No priorities.
  • A throw from any listener propagates out of emit and skips the remaining listeners.

The one event with special semantics

'error' is not an ordinary name. If an 'error' event is emitted and there is no listener for it, EventEmitter throws the error object, which on an unhandled path takes down the process. Every other event with no listeners is a silent no-op. On a request this matters: a client that disconnects mid-upload produces an error on the request, and the handler you did not attach is the difference between a logged warning and a crash.

4. Imagine the actual arrival

The network gives Node:

"hel"

Node puts it into the Readable machinery.

Then the Readable emits:

"data"

EventEmitter finds all listeners for "data":

req.on("data", callback)

and invokes:

callback("hel")

Then:

network │ ▼ "hel" │ ▼ Readable │ ▼ EventEmitter │ ▼ callback("hel")network"hel"ReadableEventEmittercallback("hel")

Next:

"lo "

and:

Readable │ ▼ EventEmitter │ ▼ callback("lo ")ReadableEventEmittercallback("lo ")

Then:

"world"

Finally:

end

The step that makes the 'data' event possible at all

A Readable does not emit 'data' just because data arrived. It emits 'data' only while readableFlowing === true, and a brand-new stream starts at null. The transition is caused by you:

  1. You call req.on('data', fn).
  2. Inside on, Readable special-cases the name 'data' and calls resume().
  3. resume() sets flowing = true and schedules the actual drain on process.nextTick — not immediately.
  4. From then on, each chunk that reaches the stream is emitted rather than buffered.

Two practical consequences of step 3. First, you can attach several listeners in the same synchronous block without losing anything, because nothing flows until the tick boundary. Second, if you attach a 'data' listener later — inside a promise callback, say — chunks that arrived in the meantime were buffered, and you will receive them in a burst rather than losing them. What you cannot do is attach it after 'end' has fired.

5. So where does the async iterator come from?

This is the really important part.

The same Readable object also implements:

[Symbol.asyncIterator]()

Therefore:

for await (const chunk of req) {
    console.log(chunk);
}

works.

The for await loop asks the request:

const iterator = req[Symbol.asyncIterator]();

Conceptually:

req │ │ [Symbol.asyncIterator]() ▼ AsyncIteratorreq[Symbol.asyncIterator]()AsyncIterator

Now we have:

iterator.next()

And next() returns a Promise.

For example:

iterator.next()

might eventually produce:

Promise → { value: "hel", done: false }

Then:

iterator.next()

might produce:

Promise → { value: "lo ", done: false }

Then:

iterator.next()

might produce:

Promise → { value: "world", done: false }

Then:

iterator.next()

might produce:

Promise → { value: undefined, done: true }

The iterator is built on 'readable' and read() — not on 'data'

This is the load-bearing detail of the whole page, because it explains why the two interfaces behave so differently under load.

Readable.prototype[Symbol.asyncIterator] never subscribes to 'data'. Its next() does this:

next() stream.read() returned a chunk? yes resolve({value,done:false}) no — park the resolver promise stays pending stream waits on its own events 'readable' → retry read(), resolve 'end' → resolve({done:true}) 'data' is never used The stream stays in paused mode the whole time it is iterated. Chunks are taken out one at a time, only when asked for. That is what makes the loop apply backpressure.

So 'readable' — the event listed once at the top of this page and then dropped — is the notification the iterator is actually built on. And because the stream stays paused, the buffer is allowed to fill, the watermark is allowed to be exceeded, and the socket is allowed to stop being read. Every one of those is a link in the backpressure chain.

6. Here's the crucial connection

There aren't necessarily two separate streams.

You have:

SAME REQUEST │ ▼ IncomingMessage │ Readable │ ┌───────────┴───────────┐ │ │ ▼ ▼ EventEmitter Async Iterator │ │ ▼ ▼ "data" next() "end" │ "error" ▼ Promise<{value,done}>SAME REQUESTIncomingMessageReadableEventEmitterAsync Iterator"data"next()"end""error"Promise<{value,done}>

They are two interfaces for consuming the same underlying stream machinery.

Two interfaces onto one buffer — which means you pick one

The diagram is right that there is one stream and one buffer. What it does not show is that a chunk, once delivered, is gone from the buffer. The two branches are not two taps on the same pipe; they are two ways of emptying it.

req.on('data', c => log(c));       // flips the stream to flowing

for await (const c of req) {       // now starved: chunks already went
  save(c);                          // to the listener above
}

Attaching the listener puts the stream in flowing mode, so chunks are pushed to the callback as they arrive and read() inside the iterator finds an empty buffer. The loop does not throw — it simply receives little or nothing and then completes at 'end'. A silent data-loss bug rather than a crash.

The accurate statement: the two interfaces are alternatives over one buffer, and choosing one is a commitment for the lifetime of that stream.

7. Now let's bring in the generator

This is where your previous work on generators becomes useful.

Imagine we implement an async iterator ourselves:

function makeIterator() {

    return {
        async next() {
            const chunk = await getNextChunk();

            return {
                value: chunk,
                done: false
            };
        }
    };

}

The consumer does:

const iterator = makeIterator();

const result = await iterator.next();

The important thing is:

consumer │ │ next() ▼ async iterator │ │ Promise ▼ consumerconsumernext()async iteratorPromiseconsumer

Now use an async generator:

async function* requestBody() {

    yield "hel";
    yield "lo ";
    yield "world";

}

This produces an object with:

const iterator = requestBody();

and:

iterator.next()

returns:

Promise │ ▼ { value: "hel", done: false }Promise{ value: "hel", done: false }

Then:

next() ↓ { value: "lo ", done: false }next(){ value: "lo ", done: false }

Then:

next() ↓ { value: "world", done: false }next(){ value: "world", done: false }

Then:

next() ↓ { value: undefined, done: true }next(){ value: undefined, done: true }

The hand-written iterator has a bug worth naming

makeIterator hard-codes done: false, so it describes an infinite sequence. As written, a for await over it never terminates. That is fine as an illustration of the protocol, but it hides the thing the protocol exists for: the iterator, not the consumer, decides when the sequence ends, and it does so by returning done: true.

The three keywords in async function*

  • function* — makes the function a generator: calling it runs no body code at all and returns a generator object parked before the first statement.
  • async — makes each next() return a promise, and permits await inside the body.
  • yield — suspends the function, preserving its entire execution context (its environment record, its position, its temporaries) somewhere other than the call stack, and hands a value out. The next next() restores that context and continues from the following statement.

The generator object it returns is both an async iterator and async iterable — it has next and it has [Symbol.asyncIterator] returning itself. That self-return is why you can feed a generator object straight to for await.

8. Now look at for await

This:

for await (const chunk of req) {
    console.log(chunk);
}

is conceptually doing:

const iterator = req[Symbol.asyncIterator]();

while (true) {

    const result = await iterator.next();

    if (result.done) {
        break;
    }

    const chunk = result.value;

    console.log(chunk);
}

That's the bridge you've been looking for.

for await │ │ calls ▼ [Symbol.asyncIterator]() │ ▼ async iterator │ │ next() ▼ Promise │ │ resolves when chunk available ▼ { value: chunk, done: false }for awaitcalls[Symbol.asyncIterator]()async iteratornext()Promiseresolves when chunk available{ value: chunk, done: false }

The desugaring with the exit path restored

The while (true) version is right about the happy path and silent about every other way out of a loop. The full shape:

const it = req[Symbol.asyncIterator]();
try {
  while (true) {
    const r = await it.next();
    if (r.done) break;
    body(r.value);            // break / return / throw all land below
  }
} finally {
  if (!completedNormally) await it.return?.();
}

On a Readable, it.return() calls destroy(). The behaviours that fall out of it:

How the loop endsEffect on the request
Runs to done: trueStream ended normally, socket free for keep-alive reuse
break after N chunksStream destroyed. The rest of the body is never read; the connection is generally closed
Body throwsSame — destroyed on the way out, and the error propagates to your caller
on('data') with a return insideNo equivalent. The listener keeps being called; you must call destroy() yourself

This is one of the strongest practical arguments for the iterator form: cleanup on the abnormal path is automatic.

9. But where does the Promise wait?

This is the beautiful part.

Suppose:

const result = await iterator.next();

but no network data has arrived yet.

The Promise is pending:

iterator.next() │ ▼ Promise pending │ │ │ network hasn't produced data │ ▼ JavaScript continuesiterator.next()Promisependingnetwork hasn't produced dataJavaScript continues

The JS thread isn't sitting there doing:

WAIT
WAIT
WAIT

Instead, the stream machinery knows:

"When data becomes available, resolve this pending operation."

Then the network produces:

"hello"

and eventually:

Readable │ ▼ pending async iterator operation │ ▼ resolve Promise │ ▼ { value: "hello", done: false }Readablepending async iterator operationresolve Promise{ value: "hello", done: false }

Then the suspended async function continues.

What "the stream machinery knows" is, concretely

It is a saved function reference. When read() comes back empty, the iterator stores the promise's own resolve in a field on itself and returns the pending promise. Nothing else happens — no timer, no polling, no loop.

The chain that eventually fires it is entirely event-driven, and each link is code that already exists for other reasons:

  1. The event loop is blocked in the OS event-polling mechanism (epoll_wait on Linux) with the socket's descriptor in the watch set.
  2. Bytes land in the kernel receive queue; the descriptor becomes readable; the poll call returns.
  3. libuv invokes the read callback, which calls recv() and hands the bytes to the HTTP parser.
  4. The parser calls push() on the request, which appends to the buffer and — because a consumer is waiting — emits 'readable'.
  5. The iterator's 'readable' handler retries read(), gets a chunk, and calls the stored resolve.
  6. Resolving schedules the continuation of your async function as a microtask. It runs after the current synchronous work finishes, before the loop moves on.

The suspended async function, throughout, is not on the call stack. Its execution context was captured when it hit await, so the stack was free to unwind all the way back to the event loop. That is precisely what makes "JavaScript continues" true.

Even the already-buffered case yields

When read() succeeds immediately, next() returns an already-resolved promise. await still suspends — it always queues a microtask before resuming. So the loop body never runs synchronously with the next() call, in either case. This is worth knowing because it means a for await loop always gives the microtask queue a turn between chunks, whereas a tight on('data') handler does not.

10. Compare EventEmitter vs Async Iterator

This is probably the most important distinction.

EventEmitter

You say:

req.on("data", chunk => {
    process(chunk);
});

Meaning:

"Push data at me whenever you have it."

The producer drives the interaction.

Readable │ │ data! ▼ callback() Readable │ │ data! ▼ callback() Readable │ │ data! ▼ callback()Readabledata!callback()Readabledata!callback()Readabledata!callback()

Async iterator

You say:

for await (const chunk of req) {
    process(chunk);
}

Meaning:

"Give me the next chunk."

Conceptually:

consumer │ │ next() ▼ iterator │ │ Promise pending │ │ ... data arrives ... │ ▼ Promise resolves │ ▼ consumer gets chunkconsumernext()iteratorPromise pending... data arrives ...Promise resolvesconsumer gets chunk

So:

EVENT MODEL producer ──────► consumer data! ASYNC ITERATOR MODEL consumer ──────► iterator next() iterator ──────► consumer resultEVENT MODELproducerconsumerdata!ASYNC ITERATOR MODELconsumeriteratornext()iteratorconsumerresult

The asymmetry the push/pull labels don't capture

Both interfaces deliver the same bytes. They differ in what happens when your handling of a chunk takes longer than the next chunk takes to arrive.

on('data')for await
Who decides timingThe producerThe consumer
Stream modeflowingpaused
Underlying event'data''readable' + read()
If your work is asyncFires again anyway — the callback's returned promise is ignoredWaits — the next next() is not issued until the body settles
BackpressureManual: pause() then resume()Automatic, as a side effect of not asking
Early exitManual destroy()break destroys the stream for you
ErrorsSeparate 'error' listener; unhandled ones can crash the processThrown from the loop, catchable with try/catch

The fourth row is where real programs break. This is a memory leak waiting to happen:

req.on('data', async chunk => {
  await writeToDatabase(chunk);   // 500ms — but nothing waits for it
});

The emitter calls the listener, the listener returns a promise at its first await, and emit discards it. Chunks keep arriving at network speed while completions happen at database speed, and the unfinished operations pile up in memory. The for await version of the same code is correct without any extra thought, because the loop cannot issue the next next() until the body's promise settles.

11. And Readable sits underneath both

This is the key architecture:

HTTP │ ▼ IncomingMessage │ ▼ Readable │ ┌──────────┴──────────┐ │ │ ▼ ▼ EventEmitter Async Iterator │ │ │ │ req.on("data") req[Symbol.asyncIterator]() │ │ ▼ ▼ callback() next() │ ▼ Promise │ ▼ {value, done}HTTPIncomingMessageReadableEventEmitterAsync Iteratorreq.on("data")req[Symbol.asyncIterator]()callback()next()Promise{value, done}

And underneath the Readable:

Readable │ ┌──────────┴──────────┐ │ │ buffer state │ │ ├── chunks ├── ended ├── chunks ├── errored └── chunks └── destroyedReadablebufferstatechunksendedchunkserroredchunksdestroyed

And underneath that:

HTTP socket │ ▼ kernel │ ▼ libuv │ ▼ Node C++ │ ▼ IncomingMessage / ReadableHTTP socketkernellibuvNode C++IncomingMessage / Readable

The layers under the stream, with their real names

The bottom stack is drawn in the direction data flows, but the labels compress several distinct components. Named precisely, bottom-up:

LayerComponentWhat it hands upward
kernelTCP receive queue for the connectionBytes, when recv() is called
libuvuv__io_poll → uv__read on a uv_stream_tA filled buffer, via a C callback
Node C++LibuvStreamWrap::OnStreamReadThe buffer, into the JS realm
Node JS — parserllhttp binding, parserOnBodyBody bytes with headers and framing stripped
Node JS — streamIncomingMessage.push(chunk)A chunk appended to _readableState.buffer
your code'data' or read()The chunk

Two things are worth extracting from that table. First, the parser sits between the socket and the request stream, which is why req yields body bytes only — the headers were consumed a layer below. Second, one socket can feed several IncomingMessage objects in sequence under keep-alive, which is why req.socket is shared state and not a per-request resource.

12. The mental model I want you to keep

Don't think:

"An IncomingMessage is an EventEmitter and an async iterator and a generator."

That's slightly misleading.

Think:

IncomingMessage │ ▼ Readable │ ┌──────────┴──────────┐ │ │ EventEmitter async iterable │ │ ▼ ▼ events push next() pull │ │ ▼ ▼ callbacks PromisesIncomingMessageReadableEventEmitterasync iterableevents pushnext() pullcallbacksPromises

EventEmitter is the notification interface.

Async iterator is the sequential consumption interface.

Readable is the underlying stream abstraction that manages the arriving data, buffering, state, flow control, and exposes those interfaces.

And this is why this code feels magical:

for await (const chunk of req) {
    console.log(chunk);
}

Because underneath, you're really doing:

req │ │ [Symbol.asyncIterator]() ▼ iterator │ │ next() ▼ Promise │ ┌──────┴──────┐ │ │ data already? data later? │ │ ▼ ▼ resolve wait for immediately stream data │ ▼ resolve Promise │ ▼ {value: chunk, done:false} │ ▼ await resumes │ ▼ loop bodyreq[Symbol.asyncIterator]()iteratornext()Promisedata already?data later?resolvewait forimmediatelystream dataresolve Promise{value: chunk, done:false}await resumesloop body

That last diagram is the connection between your async/await + Promise + async iterator + stream + EventEmitter mental models.

Notification versus consumption, made precise

The two labels are the sharpest sentences in this section, and they hold up under pressure. Sharpening them one more turn:

  • Notification answers "has something happened?" — a fact, broadcast to every subscriber, with no return channel. emit ignores what listeners return, so a listener cannot say "not yet, hold on."
  • Consumption answers "give me the next item" — a request from exactly one caller, with a return channel. The promise the consumer awaits is the return channel, and its timing is what regulates the producer.

Every difference in the comparison table above is downstream of that one structural fact: one interface has a way to say "not yet" and the other does not.

fourth question · 10:14 PMgive me a real world example thoroughly detailed with this perspective

One upload, followed all the way down

Absolutely. Let's use a real HTTP upload because it makes all four pieces visible at once:

IncomingMessage
Readable
EventEmitter
AsyncIterator
and then connect that to Promises, await, buffering and backpressure.

Imagine you have an Express/Node server:

import http from "node:http";

const server = http.createServer(async (req, res) => {
    if (req.method === "POST" && req.url === "/upload") {

        for await (const chunk of req) {
            console.log("received:", chunk);
        }

        res.end("Upload complete");
    }
});

server.listen(8080);

Now a browser uploads a 10 MB file.

Let's follow one byte of data all the way through the system.

1. The browser sends an HTTP request

The browser might send:

POST /upload HTTP/1.1
Host: example.com
Content-Length: 10485760

<10 MB of binary data>

But here's the first important thing:

The 10 MB body does not necessarily arrive at Node as one 10 MB JavaScript value.

The network might deliver pieces:

Network chunk 1 chunk 2 chunk 3 chunk 4 ↓ ↓ ↓ ↓ 64 KB 64 KB 64 KB 64 KBNetworkchunk 1chunk 2chunk 3chunk 464 KB64 KB64 KB64 KB

And potentially:

t0 t1 t2 t3 t4 AAAA BBBB CCCC DDDD ...t0t1t2t3t4AAAABBBBCCCCDDDD...

So Node needs an object capable of representing:

"This HTTP request has a body whose data is arriving over time."

That's your:

req

64 KB is an illustration, not a number in the system

There is no 64 KB anywhere on this path. Three different sizes are in play and they are easy to conflate:

SizeSet byTypical value
Segment payloadPath MTU~1460 bytes on Ethernet
Bytes per recv()libuv's read buffer and whatever the kernel has queuedUp to 64 KiB, frequently far less
Buffering thresholdhighWaterMark on the request's readable side64 KiB by default since Node 22 (16 KiB before)

So the chunk size is decided by the kernel and the arrival pattern, never by your code, and the watermark is a separate number that governs when Node stops asking for more — not the size of what it gets. Writing code that assumes any particular chunk size is the failure mode this whole design exists to prevent.

Content-Length is not the only framing

The example shows a length-delimited body, where the parser counts down from a known total. The other case behaves differently in a way you will meet:

FramingHow the end is knownConsequence
Content-Length: NParser counts N bytesTotal known up front; a truncated body is detectable as an error
Transfer-Encoding: chunkedA zero-length chunk header terminates the bodyTotal unknown until the end; trailers become possible

In both cases your for await loop is identical. The parser absorbs the difference, which is exactly the value of having a stream at this boundary.

2. What actually is req?

When you write:

http.createServer((req, res) => {
});

req is an http.IncomingMessage.

Conceptually:

req │ └── IncomingMessage │ ├── method = "POST" ├── url = "/upload" ├── headers = {...} │ └── Readable functionalityreqIncomingMessagemethod = "POST"url = "/upload"headers = {...}Readable functionality

So you can do:

req.method
req.url
req.headers

but also:

req.read()
req.on(...)
req.pipe(...)
req.destroy()

and:

req[Symbol.asyncIterator]()

That's because:

IncomingMessage │ ▼ Readable │ ▼ EventEmitterIncomingMessageReadableEventEmitter

3. Think of req as an object with multiple interfaces

This is the key.

The same object:

req

can be viewed through different interfaces.

req │ ┌────────────┼────────────┐ │ │ │ ▼ ▼ ▼ HTTP Events Iteration interface interface interface │ │ │ ▼ ▼ ▼ req.method req.on() req[Symbol.asyncIterator]() req.url req.emit() │ next()reqHTTPEventsIterationinterfaceinterfaceinterfacereq.methodreq.on()req[Symbol.asyncIterator]()req.urlreq.emit()next()

They're not separate requests.

They're different ways of interacting with the same underlying Readable stream.

The HTTP interface is a different kind of thing from the other two

The three-way split is useful, but the first branch is not a peer of the other two:

InterfaceReads fromConsumable more than once?
HTTP metadataOwn properties, already final when your handler runsYes — read req.headers as often as you like
EventsThe buffer, as it drainsNo — each chunk is delivered once
IterationThe buffer, as it drainsNo — and it competes with the events branch

Metadata is state you query. The other two are a sequence you consume. That is why "read the body twice" is not a thing you can do without buffering it yourself, while "read the method twice" is trivial.

4. The HTTP request arrives at the machine

Let's go underneath JavaScript.

Very roughly:

Browser │ │ TCP packets ▼ Network │ ▼ NIC │ ▼ Kernel socket │ ▼ libuv / Node │ ▼ IncomingMessage │ ▼ ReadableBrowserTCP packetsNetworkNICKernel socketlibuv / NodeIncomingMessageReadable

Suppose the kernel gives Node:

64 KB

of request-body data.

Node's stream machinery needs somewhere to hold it.

That's where the Readable's internal buffering comes in.

Conceptually:

Readable ┌─────────────────────────────┐ │ │ │ internal buffer │ │ │ │ [64 KB][64 KB][64 KB] │ │ │ └─────────────────────────────┘Readableinternal buffer[64 KB][64 KB][64 KB]

The exact Node internals are more complicated, but this is the right mental model.

The full descent, one link per step

Each arrow in that stack is a real handoff with a name. Following bytes down from the wire:

NIC + driver DMA into a ring buffer, then an interrupt softirq — TCP/IP stack checksums, reassembly, ACKs, into the socket's receive queue descriptor becomes readable the poll call the event loop is blocked in returns libuv — uv__read allocates a buffer, calls recv(), invokes the read callback Node C++ — OnStreamRead crosses into the JS realm llhttp parser strips headers and framing, calls back with body bytes only req.push(chunk) appended to _readableState.buffer; length grows 'readable' emitted the parked iterator resolver is found and called microtask your suspended async function resumes; loop body runs kernel C JS

Worth noticing where the boundary between "someone else's schedule" and "your schedule" falls. Everything above the parser happens whether or not your code is ready. Everything from push() down is governed by whether a consumer is asking.

The buffer is a linked list, and that is not an implementation detail

The picture shows fixed cells side by side. In reality _readableState.buffer is a BufferList — a singly linked list of Buffer objects — and the reason matters:

  • Appending is O(1) with no copying. Chunks arrive at network rate, so this is the hot path.
  • Chunks are never merged on arrival. A read() for a specific byte count may have to concatenate across nodes, but the default read() with no argument just shifts the head off.
  • The chunk you receive in the loop is the chunk the parser produced. Node does not re-slice to any size, which is why chunk boundaries look arbitrary and why they shift between runs.

5. Now introduce EventEmitter

Because Readable is an EventEmitter, you can do:

req.on("data", chunk => {
    console.log(chunk);
});

You're effectively registering:

EventEmitter "data" │ └──► callback #1EventEmitter"data"callback #1

Then data arrives.

The Readable machinery says:

"I have data."

It emits:

"data"

with the chunk.

Conceptually:

Readable │ │ emit("data", chunk) ▼ EventEmitter │ │ finds listeners ▼ callback(chunk)Readableemit("data", chunk)EventEmitterfinds listenerscallback(chunk)

So:

req.on("data", chunk => {
    console.log(chunk);
});

means:

"Whenever the stream has data to give me through the flowing/event interface, invoke this callback."

6. Now compare this with async iteration

Instead of:

req.on("data", chunk => {
    console.log(chunk);
});

you write:

for await (const chunk of req) {
    console.log(chunk);
}

This looks completely different.

But we're still consuming:

SAME req

The difference is the protocol used by the consumer.

7. for await asks for the iterator

This:

for await (const chunk of req) {
    console.log(chunk);
}

conceptually begins with:

const iterator = req[Symbol.asyncIterator]();

So:

req │ │ [Symbol.asyncIterator]() ▼ Async Iteratorreq[Symbol.asyncIterator]()Async Iterator

Now we have an iterator object.

Conceptually:

const iterator = {
    next() {
        // ...
    }
};

But because this is an async iterator, next() returns a Promise.

8. The first next()

The loop essentially does:

const result = await iterator.next();

Let's stop here.

This is enormously important.

At this exact moment:

for await │ ▼ iterator.next() │ ▼ Promisefor awaititerator.next()Promise

Maybe there is already data available.

Maybe there isn't.

A detail specific to the first call

The first next() is not quite like the rest, because up to that point readableFlowing has been null and nobody has read the socket for body bytes. So the first call typically does three things the later ones do not: it puts the stream into paused mode, triggers _read() for the first time, and finds an empty buffer. In other words the first iteration almost always takes the pending path even when the client sent everything at once — the bytes exist in the kernel, but nobody had asked Node to fetch them yet.

9. Case A: data is already buffered

Suppose the network has already delivered:

buffer ┌───────────────────────┐ │ "first 64 KB chunk" │ └───────────────────────┘buffer"first 64 KB chunk"

Then:

iterator.next()

can effectively obtain that chunk.

The Promise resolves:

{
    value: <Buffer ...>,
    done: false
}

Then:

await iterator.next()

produces:

<Buffer ...>

and therefore:

const chunk = <Buffer ...>

Then your loop body executes:

console.log("received:", chunk);

10. Case B: no data is available

This is more interesting.

Imagine the consumer asks:

await iterator.next();

but the network hasn't delivered the next chunk yet.

We have:

iterator.next() │ ▼ Promise │ ▼ PENDINGiterator.next()PromisePENDING

Nothing blocks the JavaScript thread.

We're basically saying:

"I want the next chunk. Let me know when it exists."

Then the async function suspends at:

await iterator.next();

Conceptually:

async function │ │ ▼ await iterator.next() │ ▼ SUSPENDasync functionawait iterator.next()SUSPEND

The JavaScript execution context isn't continuously executing:

while (!data) {
   wait();
}

Instead, the Promise represents the future completion.

The two cases side by side

They differ in one step, and it is not the step most people expect. Case A does not skip the suspension — it skips only the waiting.

Case A — buffer has a chunkCase B — buffer empty
read() returnsa chunknull
Promise state on returnalready fulfilledpending, resolver stored on the iterator
Does the async function suspend?yes — await always yieldsyes
What wakes itthe microtask queue, this same ticka 'readable' event, some future tick
Does the event loop get a turn?no — resumption happens before the loop advancesyes — it goes back to polling

That last row is the one with practical weight. A body that is entirely buffered will drain through your loop without the event loop ever advancing a phase, because microtasks run to exhaustion. A slow upload gives the loop a turn between every chunk.

Where the suspended function is kept

At await, the async function's execution context — its environment record, the position to resume from, its live temporaries — is preserved off the call stack, and the stack unwinds back to whatever called it. This is why "nothing blocks the JavaScript thread" is literally true rather than a figure of speech: after suspension there is no frame for that function on the stack at all. When the promise settles, the context is restored and execution continues at the statement after the await.

The while (!data) wait() version, by contrast, would keep its frame on the stack and keep the thread executing. With one thread running the event loop, that loop would also prevent the very poll call that would let the data arrive — it would not merely be wasteful, it would deadlock.

11. Meanwhile, the network continues

While our async function is suspended:

JavaScript │ │ suspended ▼ Promise pending Network │ ▼ kernel │ ▼ Node │ ▼ ReadableJavaScriptsuspendedPromise pendingNetworkkernelNodeReadable

Eventually:

network data arrives

Suppose:

"hello"

arrives.

The Readable now has data.

The async iterator machinery can satisfy the pending:

iterator.next()

operation.

So:

Promise │ │ pending │ network ──► Readable ──┤ │ ▼ resolve() │ ▼ { value: "hello", done: false }PromisependingnetworkReadableresolve(){ value: "hello",done: false }

Now the Promise becomes fulfilled.

12. This causes the await to continue

We originally had:

const chunk = await iterator.next();

It was suspended.

Now:

Promise fulfilled │ ▼ await continuation │ ▼ chunk = "hello"Promise fulfilledawait continuationchunk = "hello"

and execution resumes:

console.log(chunk);

So the flow is:

Network │ │ data arrives ▼ Readable │ │ satisfies pending next() ▼ Promise resolves │ ▼ await resumes │ ▼ loop bodyNetworkdata arrivesReadablesatisfies pending next()Promise resolvesawait resumesloop body

This is the bridge between:

network → stream → async iterator → Promise → await

"While our async function is suspended" — what the thread is doing

Nothing, in the strict sense. When there is no runnable JavaScript, the event loop reaches its poll phase and blocks in the OS event-polling mechanism (epoll_wait on Linux, kqueue on macOS, an IOCP wait on Windows). The thread is descheduled by the operating system until a descriptor it is watching becomes ready or a timeout expires. It consumes no CPU while waiting.

That is the payoff of the whole arrangement. One thread can hold thousands of suspended request handlers, because each one costs a heap-allocated context and a socket in the poll set rather than a stack and a thread.

One detail worth keeping straight: the line above says chunk = "hello", but the awaited value is the result object. await iterator.next() produces { value, done }; it is the for await loop that unwraps .value into your binding. In the desugared form the two steps are visible separately.

13. Now let's bring EventEmitter back

Here's the subtle part.

The Readable can also be consumed using:

req.on("data", ...)

So we have:

Readable │ ┌──────────────┴──────────────┐ │ │ ▼ ▼ EventEmitter Async Iterator │ │ ▼ ▼ "data" next() │ │ ▼ ▼ callback Promise │ ▼ {value, done}ReadableEventEmitterAsync Iterator"data"next()callbackPromise{value, done}

These are two different consumption interfaces.

Different consumption interfaces, one at a time

"Two different consumption interfaces" is exact. The word doing the work is consumption: each chunk leaves the buffer when it is delivered, so the two branches divide the data rather than duplicating it. Choose per stream, and choose once.

If you genuinely need two consumers, the stream layer gives you a way to say so:

GoalMechanismNote
Two independent readersreq.pipe(a); req.pipe(b);Both receive every chunk; the slower one governs
Inspect without consumingA PassThrough in the middleCosts a second buffer
Read the body twiceBuffer it yourself, then re-read your arrayGives up the memory benefit entirely

14. Push vs pull

This is the cleanest distinction.

With EventEmitter:

req.on("data", chunk => {
    process(chunk);
});

you're effectively saying:

"Push chunks into my callback when they're available."

That's push.

Readable │ │ PUSH ▼ callbackReadablePUSHcallback

With async iteration:

for await (const chunk of req) {
    process(chunk);
}

you're effectively saying:

"I am ready for the next chunk."

That's pull-oriented consumption.

consumer │ │ next() ▼ Readable / async iterator │ │ eventually provides ▼ chunkconsumernext()Readable / async iteratoreventually provideschunk

15. But the network itself is still pushing

This is where people get confused.

The HTTP network is fundamentally producing data independently of your JavaScript loop.

So there are really multiple layers:

NETWORK producer │ │ pushes bytes ▼ kernel / Node │ ▼ Readable │ │ buffers │ ▼ consumer interfaceNETWORKproducerpushes byteskernel / NodeReadablebuffersconsumer interface

Your async iterator gives you a pull-like interface over an inherently asynchronous producer.

That's why the Readable needs buffering and backpressure.

Where push turns into pull

The boundary is a single object: the buffer. Above it everything is scheduled by the producer; below it everything is scheduled by the consumer.

arrival — nobody asks for these, they simply happen _readableState.buffer the only place the two schedules meet — irregular in, regular out consumption — one next() at a time, paced by how long your loop body takes When the buffer stays full because the lower rate is slower, the links in the backpressure chain start to engage.

This reframes the earlier question of whether Readable "is" push or pull. It is the adapter between the two, and it needs a buffer for exactly the reason any adapter between mismatched rates does.

16. Now imagine your consumer is slow

Suppose:

for await (const chunk of req) {

    await expensiveDatabaseOperation(chunk);

}

Imagine the database operation takes:

500 ms

per chunk.

Meanwhile the network might be producing:

chunk 1 chunk 2 chunk 3 chunk 4 chunk 5 ...chunk 1chunk 2chunk 3chunk 4chunk 5...

You don't want Node to blindly dump unlimited data into your JavaScript process.

So the Readable has buffering and flow-control machinery.

Conceptually:

Network │ ▼ ┌────────────┐ │ Readable │ │ buffer │ └─────┬──────┘ │ ▼ consumer │ ▼ slow operationNetworkReadablebufferconsumerslow operation

If the consumer isn't keeping up:

buffer ┌─────────────────────────┐ │ chunk │ chunk │ chunk │ └─────────────────────────┘bufferchunkchunkchunk

The stream can apply backpressure upstream.

This is one of the reasons Readable is much more sophisticated than "an iterator."

Following the 500 ms case through, link by link

  1. The loop body awaits, so no further next() is issued for 500 ms.
  2. Chunks keep arriving and are appended to the buffer. _readableState.length climbs.
  3. It passes highWaterMark (64 KiB by default). Node stops calling _read(), so it stops asking the socket for more.
  4. Nothing drains the kernel receive queue for this connection, so it fills.
  5. The receive window advertised in ACKs shrinks toward zero.
  6. The sender's TCP stack stops transmitting. If the client is a browser uploading a file, its own write calls start blocking.

The whole chain works only because step 1 is true. That is the practical difference from the emitter version, where emit ignores the promise your listener returns and step 1 never happens.

The equivalent code with the emitter, written correctly

req.on('data', chunk => {
  req.pause();                          // stop the flow yourself
  expensiveDatabaseOperation(chunk)
    .then(() => req.resume())       // and restart it yourself
    .catch(err => req.destroy(err));
});

Correct, and it makes the point: everything the for await version gets from the shape of the language, the emitter version needs you to write and to not forget. The advisory word in the docs — the watermark is a threshold Node consults, not a hard cap it enforces — describes this precisely. Nothing prevents a producer from pushing past it. What the machinery does is stop asking, and only a consumer that also stops asking makes that effective.

17. What does the async generator have to do with this?

Here's where we need to distinguish async generator from async iterator.

Your req is not an async generator.

It is an object that implements an async-iteration protocol.

An async generator is one possible way to create an async iterator.

For example:

async function* chunks() {

    yield "hello";

    yield "world";

}

Calling:

const iterator = chunks();

gives you an async generator object.

It has:

iterator.next()

and:

iterator[Symbol.asyncIterator]()

So:

async generator function │ │ call ▼ async generator object │ ▼ async iterator │ ▼ next() │ ▼ Promiseasync generator functioncallasync generator objectasync iteratornext()Promise

18. Compare async generator to req

Async generator:

async function* numbers() {
    yield 1;
    yield 2;
    yield 3;
}

Its values come from:

yield

Readable:

req

Its values come from:

network / underlying source

But both can participate in:

for await (const value of something) {
}

because both provide the async-iteration protocol.

So:

for await │ needs async iterable │ ┌───────────┴───────────┐ │ │ async generator Readable │ │ yield 1 network yield 2 buffer yield 3 stream │ │ └───────────┬───────────┘ │ next() │ Promisefor awaitneeds async iterableasync generatorReadableyield 1networkyield 2bufferyield 3streamnext()Promise

Same consumer protocol. Completely different underlying machinery.

Iterable, iterator, generator — three words that are not synonyms

TermDefinitionTest
async iterableAn object with a [Symbol.asyncIterator] methodCan appear after for await … of
async iteratorAn object with a next() returning a promise of {value, done}Can be driven by hand
async generator objectBoth of the above at once, produced by calling an async function*Its [Symbol.asyncIterator] returns itself

Against that vocabulary the three objects in play sort cleanly:

req (a Readable)req[Symbol.asyncIterator]()chunks() generator object
async iterableyesyesyes
async iteratorno — has no next()yesyes
generator objectnonoyes
has a bufferyesno — reads the stream'sno
has backpressureyesinherits the stream'simplicit: the body doesn't run until asked

The row that settles the original question is the second one. req is not an async iterator — it has no next(). It is async iterable, and calling its symbol method mints an iterator that reads from it.

The bridge in the other direction

Since a generator is one way to make an async iterator, and a Readable can be built from any async iterable, you can convert between them:

const stream = Readable.from(chunks());   // generator → real stream

What that gives you is the thing the generator lacked: a buffer, a watermark, pipe, the event interface, and a destroy path. Which is the clearest possible statement of the difference — the generator supplies the sequence, and the Readable wraps it in the machinery.

19. The generator mental model

An async generator:

async function* upload() {

    const chunk1 = await getChunk();

    yield chunk1;

    const chunk2 = await getChunk();

    yield chunk2;
}

can be mentally visualized as:

generator state │ ▼ ┌───────────┐ next() ────────────►│ suspended │ └─────┬─────┘ │ await data │ ▼ yield │ ▼ Promise resolves │ ▼ {value, done:false}generator statenext()suspendedawait datayieldPromise resolves{value, done:false}

Then the generator suspends at the yield.

The next:

next()

resumes it.

That is generator state-machine behavior.

20. A Readable isn't doing exactly that

With a Readable, you shouldn't imagine:

Readable = generator

Instead:

Readable │ ├── state ├── buffer ├── events ├── flow control ├── underlying resource └── async iterator adapterReadablestatebuffereventsflow controlunderlying resourceasync iterator adapter

The async iterator is a consumption interface provided by the Readable.

That's a very important distinction.

Two suspension points, not one

The upload generator has two different kinds of pause in it, and they suspend for different reasons. Separating them is what makes the state-machine picture click.

PointSuspends becauseResumed by
await getChunk()The generator is waiting on something elseThat promise settling
yield chunk1The generator has a value and no one has asked for anotherThe consumer calling next() again

The second row is the generator's own backpressure, and it is total: between one yield and the next next(), no code in that function body runs at all. It cannot read ahead, cannot buffer, cannot pre-fetch.

Which is precisely what a Readable adds. A stream keeps reading from its resource while nobody is asking, up to the watermark, and holds the result. The generator's laziness is stricter; the stream's eagerness is bounded. That is the real content of "a Readable isn't doing exactly that."

21. The whole HTTP request

Now let's put everything together.

You have:

const server = http.createServer(async (req, res) => {

    for await (const chunk of req) {
        console.log(chunk);
    }

    res.end("done");
});

Here's the conceptual execution.

Step 1 — HTTP connection

Browser │ │ HTTP request ▼ Kernel socketBrowserHTTP requestKernel socket

Step 2 — Node receives bytes

Kernel │ ▼ libuv / NodeKernellibuv / Node

Step 3 — Node exposes the request

Node │ ▼ IncomingMessageNodeIncomingMessage

Step 4 — IncomingMessage inherits Readable

IncomingMessage │ ▼ ReadableIncomingMessageReadable

Step 5 — Readable manages incoming body

Readable │ ├── state ├── buffer ├── flow control └── eventsReadablestatebufferflow controlevents

Step 6 — for await requests async iterator

req[Symbol.asyncIterator]() │ ▼ async iteratorreq[Symbol.asyncIterator]()async iterator

Step 7 — loop calls next()

iterator.next() │ ▼ Promiseiterator.next()Promise

Step 8 — data arrives

network │ ▼ ReadablenetworkReadable

Step 9 — Promise gets fulfilled

Readable │ ▼ resolve pending next() │ ▼ Promise fulfilledReadableresolve pending next()Promise fulfilled

Step 10 — await resumes

await │ ▼ chunkawaitchunk

Step 11 — loop body runs

console.log(chunk);

Then the loop asks again:

next()

and the entire cycle repeats.

Where step 3 actually sits in time

The eleven steps read as a straight sequence, but steps 1–3 happen once per request and steps 7–11 repeat per chunk. Worth marking the boundary, and worth noting what falls between step 2 and step 3:

StepFrequencyDetail the outline compresses
1–2per connectionAlso a TCP handshake, and under keep-alive one connection serves many requests
3per requestThe parser runs first. IncomingMessage is constructed at headers-complete, before any body byte reaches it
4–5not runtime stepsThese describe the object's structure, not something that happens at this point
6once per loopAlso puts the stream into paused mode
7–11per chunkStep 8 hides the whole descent: poll → recv → parser → push → 'readable'

The one genuinely important addition: your handler is called after the headers are parsed, not after the request has arrived. Everything about the body is still in the future when your first line runs. That is why the metadata reads synchronously and the body does not.

22. The actual loop is conceptually this

Your:

for await (const chunk of req) {
    process(chunk);
}

can be mentally expanded to:

const iterator = req[Symbol.asyncIterator]();

while (true) {

    const promise = iterator.next();

    const result = await promise;

    if (result.done) {
        break;
    }

    const chunk = result.value;

    process(chunk);
}

Now look at the architecture:

IncomingMessage │ ▼ Readable │ ┌─────────────┴─────────────┐ │ │ EventEmitter Async Iterable │ │ req.on("data") Symbol.asyncIterator │ │ ▼ ▼ callback iterator │ ▼ next() │ ▼ Promise │ ▼ {value, done}IncomingMessageReadableEventEmitterAsync Iterablereq.on("data")Symbol.asyncIteratorcallbackiteratornext()Promise{value, done}

Splitting the call from the await changes nothing here — but it can elsewhere

This version separates iterator.next() from await promise, which is a useful thing to notice: the call and the wait are two different acts. On a Readable's iterator it makes no difference, because a second next() before the first settles is queued rather than run concurrently.

But the separation is exactly what you would exploit to pipeline a slow consumer — start the next request before processing the current value:

let pending = it.next();
while (true) {
  const r = await pending;
  if (r.done) break;
  pending = it.next();        // ask early…
  await process(r.value);       // …then work
}

That reintroduces exactly one chunk of read-ahead, and with it exactly one chunk of lost backpressure. Sometimes that trade is worth making. The point is that it is a trade you now have the vocabulary to make deliberately.

23. And end?

Eventually the HTTP request body is finished.

The Readable knows:

No more chunks will ever arrive.

The async iterator eventually gives:

{
    value: undefined,
    done: true
}

So:

for await (const chunk of req) {
    // ...
}

stops.

Conceptually:

chunk ↓ next() ↓ {value: chunk, done:false} ↓ chunk ↓ next() ↓ {value: chunk, done:false} ↓ ... ↓ next() ↓ {value: undefined, done:true} ↓ EXIT LOOPchunknext(){value: chunk, done:false}chunknext(){value: chunk, done:false}...next(){value: undefined, done:true}EXIT LOOP

At the EventEmitter level, the corresponding lifecycle includes:

data data data data enddatadatadatadataend

So you can see the relationship:

EventEmitter view: "data" → "data" → "data" → "end" Async iterator view: next() → next() → next() → next() ↓ ↓ ↓ ↓ value value value done:trueEventEmitter view:"data""data""data""end"Async iterator view:next()next()next()next()valuevaluevaluedone:true

They're different representations of consumption of the same stream lifecycle.

How the stream learns there is nothing more

"The Readable knows" is the end of a chain that starts at the parser, and how it knows depends on the framing:

  • Content-Length — the parser has counted down to zero.
  • chunked — the parser read a zero-length chunk header.
  • Either way it calls req.push(null). A null push is the end-of-stream signal, which sets ended, and once the buffer has drained, emits 'end'.

Two things share a name here and should not be confused. done: true is a value the iterator returns to your loop; 'end' is an event the stream emits. Under iteration the event still fires — it is what the iterator was waiting on — you just never see it.

The endings that aren't 'end'

A request body does not always finish. Three other terminations exist and only one of them looks like the diagram:

EndingEmitter viewIterator view
Body complete'end' then 'close'Loop exits normally on done: true
Client disconnects mid-body'aborted', then 'error', then 'close'The await rejects — the loop throws
You break earlynothing — you must destroy it yourselfreturn() destroys the stream on the way out
You never consume the bodynothing firesnothing runs

The second row is the one worth internalising: an aborted upload is an exception inside your for await loop, catchable with an ordinary try/catch around it. In the emitter version the same event arrives on a separate channel, and if you never attached an 'error' listener it can take the process down.

The fourth row is the quiet one. If you return a response without reading the body, the stream never ends, the socket stays unread, and the connection is held until a timeout fires. Consuming or explicitly destroying the request is not optional bookkeeping.

24. The big picture

This is the mental model I'd keep in your head:

REAL WORLD │ HTTP bytes │ ▼ TCP socket │ ▼ kernel │ ▼ libuv │ ▼ IncomingMessage (req) │ ▼ Readable ┌────────┼────────┐ │ │ │ │ │ │ buffer events iterator │ │ │ │ │ ▼ │ │ next() │ │ │ │ │ ▼ │ │ Promise │ │ │ │ │ ▼ │ │ {value,done} │ │ │ ▼ │ "data"/"end" │ │ │ ▼ │ callback │ ▼ chunksREAL WORLDHTTP bytesTCP socketkernellibuvIncomingMessage (req)Readablebuffereventsiteratornext()Promise{value,done}"data"/"end"callbackchunks

And above that:

YOUR APPLICATION EventEmitter Async Iterator │ │ ▼ ▼ req.on("data") for await (const chunk of req) │ ▼ await │ ▼ PromiseYOUR APPLICATIONEventEmitterAsync Iteratorreq.on("data")for await (const chunk of req)awaitPromise

The one sentence to remember

IncomingMessage is an HTTP-specific Readable; Readable manages the arriving byte stream and exposes multiple consumption mechanisms, including EventEmitter events and an async-iterator interface; for await uses that iterator interface, where each next() gives you a Promise that eventually resolves to { value, done }.

And the async generator is a different mechanism that also produces an async iterator—it isn't what makes req an async iterator.

The same sentence, with the four refinements folded in

The summary holds. Four clauses can be tightened using what the expansions established:

  • "an HTTP-specific Readable" — reached through four prototype links, not three: IncomingMessage → Readable → Stream → EventEmitter. Metadata is own state; everything else is found by lookup.
  • "exposes multiple consumption mechanisms" — they are alternatives over one buffer, not simultaneous views of one sequence. Committing to one for a given stream is part of the design.
  • "for await uses that iterator interface" — and that interface is built on 'readable' and read(), which keeps the stream paused. That is why iteration backpressures and 'data' does not.
  • "eventually resolves" — via a parked resolve function, woken by an event, delivered as a microtask. No polling anywhere in the chain, and the suspended function is off the call stack the entire time.

The four objects, finally distinguished

ObjectWhat it isWhat it is not
IncomingMessageHTTP metadata as own properties, plus everything a Readable isNot a container for the body
ReadableBuffer, watermark, flow state, error and end state, destroy pathNot an iterator — it has no next()
EventEmitterA name-to-handlers map and a synchronous dispatch loopNot asynchronous, and not a queue
async iteratorAn adapter minted per iteration that pulls from the bufferNot a generator, and not the stream

If one line survives from all of this: req is not an iterator, and it is not a generator. It is a stream, and a stream is the machinery that makes a pull interface possible over a producer that pushes.

Back to JIT-Atlas