A map of who is holding the data, at every layer, at every instant
Every layer of a Node program has something it calls a buffer, and they are not the same thing — some of them store your bytes, some only point at them, and one of them holds nothing at all while appearing to do the most work.
This page answers one question in one way: pick a moment in the journey, and it tells you which layer physically holds the data, who owns that memory, and whether what you are looking at is a copy, a window, or an address.
Three rules that decide every case
The confusion is not really about buffers. It is about ownership. Once you know which side of an ownership boundary a piece of memory sits on, everything else — whether it copies, whether two things can see the same bytes, whether freeing one frees the other — follows from that.
Same word, four owners
Colour is doing the work on this page: every box, chip and bar is tinted by who owns the memory, and the tint gets warmer the closer the memory is to your code. If two things share a colour they can see each other's memory directly. If they don't, something has to copy.
| Called a buffer | What it actually is | Owner | Sized by |
|---|---|---|---|
| NIC ring / device queue | Fixed slots the device writes into by DMA | driver | the driver, at boot |
| Socket receive queue | Reassembled, in-order stream bytes waiting for you to read them | kernel | autotuned, capped by the socket option |
| Socket send queue | Bytes already handed over but not yet acknowledged by the peer | kernel | autotuned, capped by the socket option |
| Page cache | File contents the kernel is holding on behalf of every process | kernel | free memory |
| The read block | A fixed-size chunk of process memory Node hands to the OS to fill | Node native | a constant, roughly 64 KB |
| The write queue | A list of addresses and lengths waiting for the socket to become writable | libuv | however much you wrote |
| A readable stream's queue | An array of chunk objects that have arrived but not been consumed | JavaScript | whatever arrived before you read |
| A writable stream's queue | An array of chunk objects accepted but not yet handed down | JavaScript | whatever you wrote before it drained |
| A chunk in your callback | A window: an offset and a length into one of the blocks above | JavaScript | what the parser or the read produced |
Step through one journey and watch custody change hands
This is the instrument the rest of the page explains. Step through a single journey and at every stop it tells you three things: who holds it, what kind of holding that is, and what you could type to see it. The ledger underneath fills as you go, so by the end you have the copy count for that path rather than a feeling about it.
—
Press forward to start the journey.
| Stop | Owner | Holding |
|---|---|---|
| empty | ||
The three kinds of holding, and why only one of them costs memory
“Where are my bytes” is ambiguous because there are three quite different ways a layer can be holding something, and only one of them means the bytes are actually there.
| Kind | What exists | Costs | Where it happens |
|---|---|---|---|
| copy | A second set of the same bytes in different memory | The full length, twice over, until one is released | Only at an ownership boundary, or when you ask for it |
| window | An object recording an offset and a length into memory that already exists | A few dozen bytes for the object, whatever the length | Everywhere inside your process |
| address | A note of where the bytes are, held by a layer that will read them later | Nothing — but the target must stay alive until it is done | The native write queue |
Because a chunk in your callback is a window and not a copy, the block underneath it stays alive for exactly as long as you keep that chunk. A small window can pin a large block.
The block the operating system filled — 65,536 bytes
Why the answer to “where is it” is sometimes one place and sometimes two
The two syscall crossings look symmetrical and are not. This is the single fact that makes the question answerable, because it tells you when the bytes are in one place and when they are in several at once.
One word, backpressure, naming three mechanisms with completely different teeth
Every layer that queues has a notion of being too full, and they are not the same notion. Knowing which one you are looking at tells you whether something will stop you, slow you, or quietly let you run out of memory.
Neither one is what its base class suggests
The request object is a readable stream that nothing ever pulls from. A normal readable asks its source for more when it runs low. This one doesn't: the HTTP parser pushes into it as bytes are parsed, and its queue is a place things are put, not a place things are fetched from. Its only real control is being able to say “stop asking the socket for more” when it is over its threshold.
The more surprising half is where the parser sits. On an HTTP connection the parser is attached below the socket's own stream machinery, so incoming bytes go from the read straight into parsing. The socket's readable queue is not in the path at all — it exists, it is empty, and reading from it is not how the request body reaches you.
The response is not a plain writable stream either. It sits above the socket and keeps a pending list of its own, because it has work to do that the socket knows nothing about: turning the status line and headers into bytes, deciding chunked framing, and grouping small writes so they leave as one segment rather than several.
The layer that appears to move all the data and stores almost none of it
This is the layer worth being precise about, because “libuv buffers the data” is the assumption that makes everything else hard to reason about. Split it by direction and it becomes simple.
| Reading | Writing | |
|---|---|---|
| Who supplies the memory | Node does, on demand, when the socket becomes readable | You did — it is the memory your chunk already points at |
| What libuv holds | Nothing after the callback returns | A queue of addresses and lengths |
| Does it copy | No — it passes your memory to the OS to fill | No — it passes your addresses to the OS to read from |
| What must stay alive | The block, for as long as anything windows onto it | Your chunk, until the write completes |
| How full it can get | Not a queue at all — one block per readable event | Unbounded; it is the pending-write backlog |
Turning the model into something you can check at three in the morning
Every layer that holds anything can be observed, and the numbers disagree with each other in useful ways. If your process memory is climbing, the layer whose number is climbing with it is the one holding your data.
| Layer | What to look at | What a large number there means |
|---|---|---|
| JS readable queue | The stream's readable length | Data arrived faster than your handler consumed it |
| JS writable queue | The stream's writable length | You are writing faster than the layer below accepts — the classic ignored return value |
| JS response pending list | The response's output size | Headers or body written before the socket could take them |
| native write queue | The handle's write queue size | The kernel refused; the socket is not writable yet |
| kernel receive queue | Send queue and receive queue columns in a socket listing | Bytes have arrived that nothing in your process has read |
| kernel send queue | The same listing, sending side | Bytes handed over and not yet acknowledged — a slow or distant peer |
| native blocks | Total process memory minus the JS heap | Blocks are being pinned by windows you are still holding |
| JS your own retention | A heap snapshot, looking at what keeps chunks reachable | You are accumulating chunks instead of streaming them |
Some of these are true
Answer before revealing. The ones you get wrong are the layers your model is still merging.