Mission 09 · Node.js Internals Campaign

http.createServer() — a browser request, llhttp, and the path back to the wire

The third lesson. Part 5 traced fs.readFile through the worker pool. Part 8 traced net.createServer through epoll. This part takes that same TCP skeleton and bolts an HTTP parser on top — the request crosses two machines, two kernels, libuv's poll phase, llhttp's state machine, and finally lands as (req, res) in your handler. Then the response retraces the path in reverse.

41 sections66 figuresbrowser to wirellhttp parser

§ 0 · The Short Answer FirstOne thread, sleeping in epoll_wait — with an HTTP parser bolted on

A Node HTTP server does not use the libuv worker pool to wait for network requests. Network I/O in Node uses an entirely different mechanism: the OS readiness / completion API.

JavaScript the program we trace
const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, World!");
});

server.listen(3000);

A browser navigates to http://localhost:3000/. We will trace the full path:

▸ short-answer-flow.txtbrowser → handler → wire
Browser machine
   ↓
Browser's network stack
   ↓
Operating system network stack
   ↓
Network (or loopback)
   ↓
Server machine
   ↓
OS kernel network stack
   ↓
Listening socket
   ↓
Accepted connection socket
   ↓
libuv readiness watcher
   ↓
Node C++ TCP wrapper
   ↓
Node HTTP parser (llhttp)
   ↓
IncomingMessage + ServerResponse JS objects
   ↓
Your JS request handler runs on the main JS thread
   ↓
res.end("Hello, World!")
   ↓
Bytes written to kernel send buffer
   ↓
TCP transmission back to browser
   ↓
Browser parses HTTP response and renders

The model

▸ readiness-model.txtregister · wait · react
register interest in a socket
   ↓
do not block
   ↓
OS notifies you when the socket is readable / writable / has a new connection
   ↓
libuv event loop wakes up
   ↓
Node reads the bytes
   ↓
Node parses HTTP
   ↓
your JS handler runs
!
Lock this in
fs.readFile() uses the libuv worker pool because filesystem syscalls can block. A Node HTTP server uses the OS readiness API (epoll / kqueue / IOCP) because the kernel already knows how to multiplex thousands of sockets without blocking — it can simply tell you when a socket has bytes ready.

The main JS thread never sits and waits on a socket. The OS does the waiting.

§ 1 · The Core ProblemTwo processes, two memories, one wire

A browser is on one machine. A Node server is on another machine (or the same machine via loopback). These two processes do not share memory. They do not share file descriptors. They do not share threads.

They communicate only by sending bytes through a network. So the problem decomposes into nine sub-problems:

  1. How do bytes leave the browser?
  2. How do bytes traverse the OS network stack?
  3. How do bytes arrive at the server's OS?
  4. How does Node know bytes have arrived?
  5. How are HTTP-formatted bytes parsed into req/res objects?
  6. How does Node invoke your JS handler?
  7. How does your JS write a response?
  8. How do those response bytes go back to the browser?
  9. How does the browser interpret them?

Every layer adds a transformation.

Boxed problem layering

▸ problem-layers.txtbrowser → handler · 6 layers
┌────────────────────────────────────────────┐
│ Browser application                        │
│                                            │
│ "fetch http://localhost:3000/"             │
└─────────────────────┬──────────────────────┘
                      │ wants to send a request
                      ▼
┌────────────────────────────────────────────┐
│ Browser HTTP/networking implementation     │
│                                            │
│ - format HTTP request bytes                │
│ - decide which connection to use           │
└─────────────────────┬──────────────────────┘
                      │ uses TCP
                      ▼
┌────────────────────────────────────────────┐
│ OS network stack (browser machine)         │
│                                            │
│ - TCP connection state                     │
│ - send / receive buffers                   │
│ - IP routing                               │
└─────────────────────┬──────────────────────┘
                      │ bytes on the wire / loopback
                      ▼
┌────────────────────────────────────────────┐
│ OS network stack (server machine)          │
│                                            │
│ - listening socket                         │
│ - accept queue                             │
│ - new connection socket                    │
│ - receive buffer                           │
└─────────────────────┬──────────────────────┘
                      │ readiness notification
                      ▼
┌────────────────────────────────────────────┐
│ libuv event loop                           │
│                                            │
│ - watches sockets via epoll/kqueue/IOCP    │
│ - reads bytes when readable                │
└─────────────────────┬──────────────────────┘
                      │
                      ▼
┌────────────────────────────────────────────┐
│ Node native HTTP layer                     │
│                                            │
│ - HTTP parser (llhttp)                     │
│ - IncomingMessage / ServerResponse         │
└─────────────────────┬──────────────────────┘
                      │
                      ▼
┌────────────────────────────────────────────┐
│ JavaScript request handler                 │
│                                            │
│ (req, res) => { res.end("...") }           │
└────────────────────────────────────────────┘
!
Lock this in
An HTTP request is just bytes that follow a specific format, sent over a TCP connection that was established beforehand. A Node HTTP server is a TCP server with an HTTP parser bolted on top.

§ 2 · The Full Ownership ChainJavaScript at the top, the kernel doing the real work

Memorize the layers. The "socket" in your JavaScript program is the top of the stack. The actual TCP connection state is owned and driven by the OS kernel. Node's role is to ask the kernel "are there bytes yet?" via libuv's readiness watcher, then read them when the kernel says yes.

Arrow flow (top to bottom)

▸ ownership-chain.txtflow · top-down
JavaScript API
   ↓
Node JS http module
   ↓
Node JS net module
   ↓
Node C++ TCP wrapper
   ↓
libuv TCP handle
   ↓
OS readiness API (epoll / kqueue / IOCP)
   ↓
OS kernel network stack
   ↓
Network interface / loopback
   ↓
(other machine's stack)
   ↓
completion path back upward
   ↓
Node HTTP parser (llhttp)
   ↓
IncomingMessage / ServerResponse objects
   ↓
JS request handler runs on the main JS thread

Boxed ownership stack

▸ ownership-chain-boxed.txtlayered stack
┌────────────────────────────────────────────┐
│ JavaScript API                             │
│                                            │
│ http.createServer((req, res) => {...})     │
│ server.listen(3000)                        │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Node JS http module                        │
│                                            │
│ - Server class                             │
│ - IncomingMessage class                    │
│ - ServerResponse class                     │
│ - HTTP parser glue                         │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Node JS net module                         │
│                                            │
│ - net.Server                               │
│ - net.Socket                               │
│ - listening / connection bookkeeping       │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Node C++ binding layer                     │
│                                            │
│ - TCPWrap (listening)                      │
│ - TCPWrap (per connection)                 │
│ - StreamWrap read/write hooks              │
│ - HTTPParser binding (llhttp)              │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ libuv                                      │
│                                            │
│ - uv_tcp_t handle                          │
│ - uv_listen / uv_accept                    │
│ - uv_read_start                            │
│ - event loop poll phase                    │
│ - epoll/kqueue/IOCP integration            │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ OS kernel network stack                    │
│                                            │
│ - listening socket state                   │
│ - SYN queue / accept queue                 │
│ - per-connection TCP state machine         │
│ - send / receive buffers                   │
│ - IP layer                                 │
│ - device driver                            │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Network interface / loopback               │
│                                            │
│ - NIC hardware                             │
│ - or in-memory loopback path               │
└────────────────────────────────────────────┘
!
Key invariant
The "socket" in your JavaScript program is the top of the stack. The actual TCP connection state is owned and driven by the OS kernel. Node's role is to ask the kernel "are there bytes yet?" via libuv's readiness watcher, then read them when the kernel says yes.

§ 3 · Start From CPU RealityThe CPU never understood "the internet"

The CPU does not understand HTTP, TCP, IP, sockets, request handlers, or req and res objects. The CPU only executes machine instructions.

▸ instruction-cycle.txtcycle
fetch instruction
   ↓
decode instruction
   ↓
execute instruction
   ↓
move to next instruction
   ↓
repeat

Networking is built up by layers of code (and sometimes hardware) that, ultimately, just shuffle bytes between memory regions and notify each other when something has happened.

▸ network-bottom.txtat the bottom
┌────────────────────────────────────────────┐
│ At the bottom                              │
│                                            │
│ - bytes sitting in RAM                     │
│ - copied to NIC by DMA                     │
│ - serialized as electrical / optical /     │
│   radio signals                            │
│ - received by a NIC on the other side      │
│ - DMA'd back into RAM                      │
│ - lifted up through layers of software     │
└────────────────────────────────────────────┘

For loopback (127.0.0.1 or localhost), the bytes never leave the machine — they go through an in-memory path provided by the kernel.

!
Lock this in
A "network request" is, mechanically, the act of arranging bytes in one machine's memory so the OS will deliver them to another machine's memory, and arranging notification so software on each side can react.

§ 4 · Process vs ThreadNetworking lens — two processes, one kernel

When you run node server.js, the OS creates a Node process. When you open a browser, the OS has one or more browser processes. These two processes are completely separate.

Two-process view

▸ two-processes.txtbrowser + node + kernel
┌────────────────────────────────┐    ┌────────────────────────────────┐
│ Browser Process                │    │ Node Process                   │
│                                │    │                                │
│  - own memory                  │    │  - own memory                  │
│  - own fd table                │    │  - own fd table                │
│  - own threads                 │    │  - own threads                 │
│  - own sockets                 │    │  - own sockets                 │
│                                │    │                                │
│  Cannot directly read Node's   │    │  Cannot directly read          │
│  memory                        │    │  browser's memory              │
└──────────────┬─────────────────┘    └──────────────┬─────────────────┘
               │                                     │
               └──────────── OS kernel ──────────────┘
                              │
                              ▼
                    ┌────────────────────┐
                    │ Network stack      │
                    │                    │
                    │ Mediates ALL       │
                    │ communication      │
                    │ between processes  │
                    └────────────────────┘
!
Lock this in
Two processes never directly touch each other. The kernel is always the intermediary. For our HTTP request, the kernel is the postal service that physically moves the bytes between the browser and Node.

§ 5 · The Two SidesWhat each process owns · what the kernel owns

Browser side

▸ browser-owns.txtbrowser process
┌────────────────────────────────────────────┐
│ Browser Process                            │
│                                            │
│  - URL parser                              │
│  - DNS resolver                            │
│  - HTTP/1.1, HTTP/2, HTTP/3 implementation │
│  - TLS layer (for https)                   │
│  - connection pool                         │
│  - cookie jar                              │
│  - cache                                   │
│  - rendering engine                        │
│  - JS engine                               │
└────────────────────────────────────────────┘

Server (Node) side

▸ node-owns.txtnode process
┌────────────────────────────────────────────┐
│ Node Process                               │
│                                            │
│  Main JS Thread:                           │
│   - V8                                     │
│   - http module                            │
│   - net module                             │
│   - your request handler                   │
│                                            │
│  libuv:                                    │
│   - event loop                             │
│   - TCP handles                            │
│   - epoll/kqueue/IOCP integration          │
│                                            │
│  Worker pool:                              │
│   - NOT involved in network I/O            │
└────────────────────────────────────────────┘

OS kernel (on each side)

▸ kernel-owns.txtkernel responsibilities
┌────────────────────────────────────────────┐
│ Kernel                                     │
│                                            │
│  - sockets                                 │
│  - TCP state machine                       │
│  - send / receive buffers                  │
│  - listening socket / accept queue         │
│  - port allocation                         │
│  - IP routing                              │
│  - device drivers                          │
└────────────────────────────────────────────┘
!
Lock this in
Each side has its own user-space code, its own kernel, its own buffers. The HTTP request crosses all of these layers twice — once on the way out, once on the way back.

§ 6 · What Is a Socket?A small integer pointing at kernel state

A socket is, like a file descriptor, a process-local handle to a kernel-managed resource. For a TCP socket, that resource is a TCP endpoint: an IP address, a port, and a state machine.

C syscall signature
fd = socket(AF_INET, SOCK_STREAM, 0);

Socket relationship

▸ socket-relationship.txtfd → kernel object → struct sock
┌──────────────────────────────────────────┐
│ Process-local fd table                   │
│                                          │
│ fd 21                                    │
└─────────────────────┬────────────────────┘
                      │ points to
                      ▼
┌──────────────────────────────────────────┐
│ Kernel "open file" entry (for socket)    │
│                                          │
│ - type: SOCK_STREAM (TCP)                │
│ - flags                                  │
│ - reference to socket structure          │
└─────────────────────┬────────────────────┘
                      │ refers to
                      ▼
┌──────────────────────────────────────────┐
│ Kernel TCP socket structure              │
│                                          │
│ - local address (IP, port)               │
│ - remote address (IP, port) (if connected)│
│ - state (LISTEN, ESTABLISHED, etc.)      │
│ - send buffer                            │
│ - receive buffer                         │
│ - sequence numbers                       │
│ - timers                                 │
└──────────────────────────────────────────┘

A socket fd is just an integer in your process. The actual TCP machinery lives in the kernel.

!
Lock this in
When Node code says "the socket sent data," what physically happened is: Node copied bytes into the kernel's send buffer for that socket. The kernel later transmitted those bytes according to TCP rules. The socket fd is just the handle that lets Node refer to that kernel-side state.

§ 7 · Listening Socket vs Connection SocketOne parent · many children

This is one of the most misunderstood ideas in network programming. A server has two kinds of sockets:

The listening socket

  • one socket
  • bound to a port (e.g. 3000)
  • in LISTEN state
  • does NOT carry data
  • accepts incoming connections

The connection sockets

  • one PER active client connection
  • bound to (server IP : 3000) on local side
  • bound to (client IP : random port) on remote side
  • in ESTABLISHED state
  • carries data for that ONE client

Server fd table

▸ server-fd-table.txtone listener · many children
┌────────────────────────────────────────────┐
│ Server Node Process                        │
│                                            │
│  fd 7  ── Listening socket                 │
│           local: 0.0.0.0:3000              │
│           state: LISTEN                    │
│                                            │
│  fd 12 ── Connection from browser A        │
│           local:  127.0.0.1:3000           │
│           remote: 127.0.0.1:54321          │
│           state: ESTABLISHED               │
│                                            │
│  fd 15 ── Connection from browser B        │
│           local:  127.0.0.1:3000           │
│           remote: 127.0.0.1:54322          │
│           state: ESTABLISHED               │
│                                            │
│  fd 19 ── Connection from another client   │
│           local:  127.0.0.1:3000           │
│           remote: 192.168.1.5:60001        │
│           state: ESTABLISHED               │
└────────────────────────────────────────────┘

How a connection socket comes into existence

▸ connection-birth.txthandshake → accept → new fd
client tries to connect
   ↓
TCP handshake reaches kernel
   ↓
kernel queues connection on listening socket's accept queue
   ↓
server calls accept()
   ↓
kernel removes connection from accept queue
   ↓
kernel creates a NEW socket fd for that connection
   ↓
server now has a per-connection socket
!
Lock this in
server.listen(3000) produces one listening socket. Every browser request produces an additional connection socket. The listening socket itself never carries HTTP data — it only produces children.

§ 8 · The OS Network StackApplication · transport · network · link

When the browser sends bytes, here is the layered processing:

▸ stack-out.txtsend-side layering
HTTP request bytes
   ↓
TCP layer — adds sequence numbers, ports, checksum
   ↓
IP layer — adds source/destination IP, routing decision
   ↓
Link layer — adds MAC addresses (ethernet/wifi) or skipped on loopback
   ↓
Device driver — hands frame to NIC (or loopback path)
   ↓
Wire / RF / fiber / loopback memory

On the receiving side, the layers run in reverse.

Boxed network stack

▸ network-stack.txt4 layers · plus hardware
┌──────────────────────────────────────────┐
│ Application layer                        │
│                                          │
│ - HTTP request bytes                     │
│ - "GET / HTTP/1.1\r\nHost: ..."          │
└─────────────────────┬────────────────────┘
                      │ written into socket
                      ▼
┌──────────────────────────────────────────┐
│ Transport layer (TCP)                    │
│                                          │
│ - segments bytes into TCP segments       │
│ - manages sequence numbers               │
│ - manages retransmission                 │
│ - flow control / congestion control      │
└─────────────────────┬────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────┐
│ Network layer (IP)                       │
│                                          │
│ - wraps segments in IP packets           │
│ - chooses route                          │
│ - handles fragmentation if needed        │
└─────────────────────┬────────────────────┘
                      │
                      ▼
┌──────────────────────────────────────────┐
│ Link layer                               │
│                                          │
│ - frames packets                         │
│ - addresses by MAC                       │
│ - or uses loopback path in-memory        │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Hardware / loopback                      │
│                                          │
│ - NIC                                    │
│ - or kernel loopback driver              │
└──────────────────────────────────────────┘

For our example (localhost:3000), the stack is shorter in practice — bytes pass through the loopback driver instead of leaving the machine — but the logical layering is identical.

§ 9 · The Browser SideFrom URL to bytes on the wire

We will not deeply trace the browser's internals (Node is the focus), but we need a clear mental model. When the browser hits http://localhost:3000/:

  1. Browser parses URL
  2. Browser checks connection pool — any open conn to localhost:3000?
  3. If not, browser performs DNS lookup — "localhost" → 127.0.0.1
  4. Browser asks kernel for a TCP socket
  5. Browser kernel performs TCP three-way handshake with server
  6. Browser formats HTTP request bytes
  7. Browser writes bytes into kernel's send buffer
  8. Kernel transmits bytes

Arrow flow — browser process and its kernel

▸ browser-side-flow.txtfetch → connect → write
BROWSER PROCESS                         BROWSER OS KERNEL

fetch("http://localhost:3000/")
        │
        ├── parse URL
        ├── DNS lookup ─────────────────▶ resolver / cache
        │  ◀────────────── 127.0.0.1
        │
        ├── socket() ──────────────────▶ create TCP socket fd
        ├── connect(127.0.0.1:3000) ───▶ TCP handshake begins
        │                                  │
        │                                  ▼
        │                              SYN sent to server
        │                              SYN-ACK received
        │                              ACK sent
        │                                  │
        │  ◀────────────── connected
        │
        ├── format request bytes
        ├── write(fd, bytes, len) ─────▶ bytes copied into send buffer
        │                                  │
        │                                  ▼
        │                              transmitted via loopback / NIC
!
Lock this in
By the time we look at the server side, the browser has already resolved the address, opened a TCP connection, and placed the HTTP request bytes into its kernel send buffer.

§ 10 · The TCP Three-Way HandshakeTwo kernels alone — Node hears nothing yet

Before a single HTTP byte travels, TCP must establish a connection.

Step by step

▸ tcp-handshake.txtSYN · SYN-ACK · ACK
BROWSER (CLIENT)                       SERVER

socket()
connect(IP, 3000)
        │
        ├── send SYN ───────────────────▶ kernel receives SYN
        │                                  │
        │                                  ├── if listening socket exists on port 3000:
        │                                  │       add half-open conn to SYN queue
        │                                  │
        │  ◀───────────── send SYN-ACK
        │
        ├── send ACK ───────────────────▶ kernel marks connection complete
        │                                  │
        │                                  └── moves connection to accept queue
        │
connect() returns

Server-side queues during handshake

▸ syn-accept-queues.txttwo queues per listener
┌──────────────────────────────────────────┐
│ Server kernel — listening socket fd 7    │
│                                          │
│ ┌──────────────────────────────────────┐ │
│ │ SYN queue (incomplete handshakes)    │ │
│ │                                      │ │
│ │ - half-open conn from 127.0.0.1:54321│ │
│ └──────────────────────────────────────┘ │
│                                          │
│ ┌──────────────────────────────────────┐ │
│ │ Accept queue (completed handshakes)  │ │
│ │                                      │ │
│ │ - ready conn from 127.0.0.1:54322    │ │
│ │ - ready conn from 192.168.1.5:60001  │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────┘

When Node calls accept(), the kernel:

  • removes one entry from the accept queue
  • returns a new fd representing that established connection
!
Lock this in
The TCP handshake happens entirely between the two kernels. Node has not yet been notified. Node only learns about this connection when libuv asks the kernel "anything new on the listening socket?" and the kernel says "yes."

§ 11 · The HTTP Request BytesJust text · sitting on top of TCP

Before transmitting, the browser builds bytes that look like this:

HTTP/1.1 request bytes (literal)
GET / HTTP/1.1\r\n
Host: localhost:3000\r\n
User-Agent: Mozilla/5.0 ...\r\n
Accept: text/html,...\r\n
Accept-Language: en-US\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
\r\n

The blank line (\r\n immediately after the last header) signals end of headers. For a GET request there is no body.

Boxed view of the request

▸ request-anatomy.txtstart-line · headers · CRLF · body
┌────────────────────────────────────────────┐
│ HTTP request bytes (one big byte buffer)   │
│                                            │
│  Request line:                             │
│    GET / HTTP/1.1\r\n                      │
│                                            │
│  Headers:                                  │
│    Host: localhost:3000\r\n                │
│    User-Agent: ...\r\n                     │
│    Accept: ...\r\n                         │
│    ...                                     │
│                                            │
│  End of headers:                           │
│    \r\n                                    │
│                                            │
│  Body (if any):                            │
│    (empty for GET)                         │
└────────────────────────────────────────────┘
!
Lock this in
HTTP is just a text-shaped byte format sitting on top of TCP. There is no magic. A parser scans these bytes and extracts method, path, version, headers, and body.

§ 12 · What server.listen(3000) Actually DoesIt does not wait — it registers and returns

Now we move to the Node side. Code:

JavaScript server setup
const server = http.createServer((req, res) => { /* ... */ });
server.listen(3000);

server.listen(3000) is a JavaScript call on the main JS thread. The chain:

▸ listen-flow.txtJS → C++ → libuv → kernel
http.Server.listen(3000)
   ↓
delegates to net.Server.listen(3000)
   ↓
Node JS net wrapper
   ↓
Node C++ TCPWrap binding
   ↓
libuv uv_tcp_t handle
   ↓
OS syscalls: socket() / bind() / listen()
   ↓
kernel creates listening socket
   ↓
libuv registers interest with epoll/kqueue/IOCP

Boxed call chain

▸ listen-call-chain.txttop to bottom · seven layers
┌──────────────────────────────────────────┐
│ JavaScript                               │
│                                          │
│ server.listen(3000)                      │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Node JS http module                      │
│                                          │
│ - delegates to net.Server                │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Node JS net module                       │
│                                          │
│ - sets up listener bookkeeping           │
│ - calls into C++ TCPWrap                 │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Node C++ binding (TCPWrap)               │
│                                          │
│ - allocates a uv_tcp_t handle            │
│ - calls uv_tcp_bind                      │
│ - calls uv_listen                        │
│ - registers connection callback          │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ libuv                                    │
│                                          │
│ - performs socket() / bind() / listen()  │
│ - registers fd with epoll/kqueue/IOCP    │
│ - asks kernel: "wake me when readable    │
│   on this fd"                            │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ OS Kernel                                │
│                                          │
│ - allocates socket structure             │
│ - assigns local address 0.0.0.0:3000     │
│ - sets state to LISTEN                   │
│ - prepares accept queue                  │
└──────────────────────────────────────────┘
!
Lock this in
server.listen(3000) does not "wait for connections." It tells the kernel: "I will be the owner of port 3000. Queue incoming connections for me, and tell libuv when one is ready." Then it returns immediately.

§ 13 · The bind() and listen() SyscallsWhat the kernel actually does

What does the kernel actually do when libuv issues these three syscalls?

▸ syscall-flow.txtsocket → bind → listen
socket(AF_INET, SOCK_STREAM, 0)
   ↓
returns fd 7 — a fresh TCP socket structure
   ↓
bind(fd, "0.0.0.0:3000")
   ↓
kernel checks: is port 3000 already in use?
   ↓
   if yes: EADDRINUSE error
   if no: assign port to this socket
   ↓
listen(fd, backlog)
   ↓
kernel marks the socket as LISTEN
   ↓
kernel allocates SYN queue and accept queue

Kernel state after listen()

▸ state-after-listen.txtsocket fd 7 · LISTEN
┌────────────────────────────────────────────┐
│ Kernel — socket fd 7 in Node process       │
│                                            │
│  type:        TCP (SOCK_STREAM)            │
│  state:       LISTEN                       │
│  local addr:  0.0.0.0:3000                 │
│  remote addr: (none)                       │
│                                            │
│  SYN queue:    [empty]                     │
│  Accept queue: [empty]                     │
│                                            │
│  Will produce: new connection sockets      │
│                upon accept()               │
└────────────────────────────────────────────┘
!
Lock this in
After listen(), the kernel is now the active component of the server. Node does nothing. The kernel waits for SYN packets and processes handshakes. When connections complete, they accumulate in the accept queue.

§ 14 · The OS Readiness API — epoll, kqueue, IOCPOne thread · thousands of sockets

This is the crucial difference from fs.readFile(). For a file read, libuv hands the work to a worker thread, which blocks in read(). For network sockets, libuv does not need a worker thread, because the OS itself provides a way to ask:

"Of these N sockets I care about, which ones are ready right now?"

This is multiplexing.

Linux: epoll

▸ epoll-syscalls.txtcreate · ctl · wait
epoll_create()  → creates an epoll instance
epoll_ctl(fd)   → register a fd with the epoll instance
epoll_wait()    → block until any registered fd is ready

macOS / BSD: kqueue

▸ kqueue-syscalls.txtcreate · register · wait
kqueue()        → creates an event queue
kevent(...)     → register interest, or wait for events

Windows: IOCP

▸ iocp-model.txtcompletion-based
- different model: completion-based instead of readiness-based
- you submit operations; the kernel completes them and notifies you

libuv abstracts these differences. From Node's perspective: "register fd with libuv → libuv will tell me when something happens."

The multiplexing model

▸ multiplexing.txtone thread · N fds
┌────────────────────────────────────────────┐
│ Node Process — Main JS Thread / libuv      │
│                                            │
│  Registered sockets:                       │
│    fd 7  (listening)                       │
│    fd 12 (connection A)                    │
│    fd 15 (connection B)                    │
│    fd 19 (connection C)                    │
│                                            │
│  ┌──────────────────────────────────────┐  │
│  │ epoll_wait()                         │  │
│  │                                      │  │
│  │ Blocks until ANY of those fds        │  │
│  │ becomes ready.                       │  │
│  └──────────────────────────────────────┘  │
└─────────────────────┬──────────────────────┘
                      │
                      ▼
┌────────────────────────────────────────────┐
│ Kernel                                     │
│                                            │
│  - tracks readiness of every registered fd │
│  - when any becomes readable, marks ready  │
│  - returns to epoll_wait caller            │
└────────────────────────────────────────────┘
!
Lock this in
The kernel can monitor thousands of sockets simultaneously and notify your single thread when any of them needs attention. That is why a Node server can handle many concurrent connections without many threads. The OS does the waiting.

§ 15 · The libuv Event Loop — Poll PhaseWhere Node sleeps when there's nothing to do

The libuv event loop has phases. The phase that matters here is the poll phase.

▸ event-loop-phases.txtsimplified iteration
loop iteration:

1. timers
2. pending callbacks
3. idle / prepare
4. poll  ◄── epoll_wait / kevent / IOCP wait happens here
5. check (setImmediate)
6. close callbacks

Arrow flow inside one iteration

▸ poll-iteration.txttimers → poll → check → close
event loop iteration begins
   ↓
run any due timers
   ↓
run pending callbacks
   ↓
enter POLL phase
   ↓
if there are watched I/O handles:
   call epoll_wait / kevent / IOCP
   ↓
   block until at least one fd is ready
   OR timeout (next timer due)
   ↓
   for each ready fd:
     run the libuv I/O callback
     which runs the Node C++ callback
     which (eventually) runs JS
   ↓
run check callbacks (setImmediate)
   ↓
run close callbacks
   ↓
next iteration

Boxed loop view

▸ event-loop-blocks.txtmain JS thread · sleeping in poll
┌──────────────────────────────────────────┐
│ libuv event loop (main JS thread)        │
│                                          │
│  while (alive) {                         │
│    run_timers();                         │
│    run_pending();                        │
│    run_idle_prepare();                   │
│                                          │
│    poll_for_io();   // ◄── waits here    │
│                                          │
│    run_check();                          │
│    run_close();                          │
│  }                                       │
└──────────────────────────────────────────┘
!
Lock this in
When Node has nothing to do, the main JS thread is inside the kernel in epoll_wait (or equivalent), sleeping. It is not spinning. It is not polling. It is genuinely asleep, costing the CPU nothing. The kernel wakes it the instant any registered fd becomes ready.

§ 16 · Connection Arrives — What the Kernel DoesHandshake without JS involvement

The browser sends a SYN.

▸ kernel-connection-flow.txtSYN → handshake → epoll signal
Wire / loopback ─── SYN ──▶ server NIC / loopback driver
   ↓
kernel network stack receives SYN
   ↓
kernel finds that fd 7 (LISTEN on port 3000) matches
   ↓
kernel allocates half-open connection state
   ↓
kernel sends SYN-ACK
   ↓
browser sends ACK
   ↓
kernel completes handshake
   ↓
kernel moves connection from SYN queue to accept queue
   ↓
kernel marks fd 7 as "readable"
   ↓
   (because there's now an acceptable connection on it)
   ↓
kernel signals epoll/kqueue/IOCP that fd 7 is ready

Accept queue state

▸ accept-queue-state.txtbefore vs after handshake
Before connection arrives:

┌──────────────────────────────────────────┐
│ Listening fd 7                           │
│   state: LISTEN                          │
│                                          │
│   accept queue: [empty]                  │
│                                          │
│   Ready signal? No                       │
└──────────────────────────────────────────┘


After handshake completes:

┌──────────────────────────────────────────┐
│ Listening fd 7                           │
│   state: LISTEN                          │
│                                          │
│   accept queue:                          │
│     [conn A: 127.0.0.1:54321]            │
│                                          │
│   Ready signal? YES                      │
└──────────────────────────────────────────┘

The kernel notifies epoll. The main JS thread, sleeping in epoll_wait, wakes up.

!
Lock this in
The kernel does the entire handshake without Node being involved, then signals readiness on the listening socket fd.

§ 17 · libuv Reacts — Calls accept()From kernel signal to 'connection' event

The main JS thread is now back in user space, inside libuv's poll phase.

Step by step

▸ libuv-accept-flow.txtepoll wake → accept → emit
epoll_wait returns
   ↓
"fd 7 is readable"
   ↓
libuv looks up handle for fd 7
   ↓
calls registered I/O callback for that handle
   ↓
that callback is "on_connection"
   ↓
on_connection calls accept(fd 7)
   ↓
kernel removes one entry from accept queue
   ↓
kernel allocates a NEW socket structure
   ↓
returns new fd (e.g. fd 12) to libuv
   ↓
libuv creates a uv_tcp_t handle for fd 12
   ↓
libuv invokes Node C++ connection callback
   ↓
Node C++ creates a TCPWrap object for fd 12
   ↓
Node C++ calls back into JS: net.Server emits 'connection'
   ↓
JS http server's 'connection' listener runs
   ↓
http server attaches an HTTP parser to the new socket
   ↓
http server tells libuv: "start reading on fd 12"

Socket relationship after accept

▸ post-accept-socket.txtfd 12 wrapped at every layer
┌────────────────────────────────────────────┐
│ Node Process                               │
│                                            │
│  fd 7  ── Listening socket (still LISTEN)  │
│                                            │
│  fd 12 ── New connection socket            │
│           state: ESTABLISHED               │
│           remote: 127.0.0.1:54321          │
│                                            │
│           Wrapped by:                      │
│             libuv uv_tcp_t                 │
│           Wrapped by:                      │
│             Node C++ TCPWrap               │
│           Wrapped by:                      │
│             JS net.Socket                  │
│           With:                            │
│             HTTP parser attached           │
└────────────────────────────────────────────┘
!
Lock this in
accept() does not block here. The kernel already had a complete connection sitting in the accept queue. accept() simply hands it to Node and returns a new fd.

§ 18 · The Wrapper Layers Around a ConnectionFive names · one socket

Once accepted, a single TCP connection is represented at multiple layers:

Wrapper stack for ONE connection

▸ connection-wrappers.txtJS → C++ → libuv → kernel
┌────────────────────────────────────────────┐
│ JavaScript                                 │
│                                            │
│  IncomingMessage (req)  ◄── stream         │
│  ServerResponse  (res)  ◄── stream         │
│  parser state (llhttp)                     │
└─────────────────────┬──────────────────────┘
                      │ both wrap the same socket
                      ▼
┌────────────────────────────────────────────┐
│ JavaScript                                 │
│                                            │
│  net.Socket (a Duplex stream)              │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Node C++                                   │
│                                            │
│  TCPWrap                                   │
│  - read callbacks                          │
│  - write requests                          │
│  - links JS object to libuv handle         │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ libuv                                      │
│                                            │
│  uv_tcp_t handle                           │
│  - holds fd                                │
│  - registered with epoll/kqueue/IOCP       │
└─────────────────────┬──────────────────────┘
                      ▼
┌────────────────────────────────────────────┐
│ Kernel                                     │
│                                            │
│  socket structure (fd 12)                  │
│  - TCP state machine                       │
│  - send buffer                             │
│  - receive buffer                          │
└────────────────────────────────────────────┘
!
Lock this in
req and res in your handler are JavaScript streams that ultimately read from and write to the same kernel TCP socket. The HTTP parser sits between the raw byte stream and the JS-level req object.

§ 19 · uv_read_start — Bytes Will Trigger Callbacks"Watch fd 12 for readability"

Right after the connection is set up, Node tells libuv: "Watch fd 12 for readability. When bytes arrive, give them to me." This is uv_read_start.

Internally

▸ uv-read-start.txtregister · wait · callback
uv_read_start(handle)
   ↓
libuv updates epoll/kqueue/IOCP registration
   ↓
"now also notify on readable for fd 12"

Registration state after uv_read_start

▸ epoll-after-readstart.txttwo fds watched
┌──────────────────────────────────────────┐
│ epoll registration                       │
│                                          │
│  fd 7  → readable? notify (accept queue) │
│  fd 12 → readable? notify (data arrived) │
│  fd 12 → writable? notify (when needed)  │
└──────────────────────────────────────────┘
!
Lock this in
There is no thread sitting on the socket. There is just a kernel-side flag: "tell me when fd 12 has bytes."

§ 20 · The Browser's HTTP Request Bytes ArriveReceive buffer fills · epoll fires

Now the browser sends:

HTTP/1.1 on the wire
GET / HTTP/1.1\r\n
Host: localhost:3000\r\n
...\r\n
\r\n

Arrow flow — kernel-to-kernel

▸ bytes-arrive.txtbrowser send → server recv
BROWSER KERNEL                         SERVER KERNEL

bytes leave send buffer
        │
        ├── travel via loopback
        │     or NIC → wire → NIC
        │
        ▼
                                     bytes arrive at receive path
                                            │
                                            ├── TCP layer reorders if needed
                                            ├── delivers to socket fd 12 receive buffer
                                            │
                                            ├── marks fd 12 as readable
                                            │
                                            └── notifies epoll

Kernel buffer state

▸ recv-buffer-state.txtfd 12 · receive buffer
┌──────────────────────────────────────────┐
│ Kernel — socket fd 12                    │
│                                          │
│  Receive buffer:                         │
│    ┌────────────────────────────────┐    │
│    │ "GET / HTTP/1.1\r\n"           │    │
│    │ "Host: localhost:3000\r\n"     │    │
│    │ "User-Agent: ...\r\n"          │    │
│    │ "Accept: ...\r\n"              │    │
│    │ "\r\n"                         │    │
│    └────────────────────────────────┘    │
│                                          │
│  State: ESTABLISHED                      │
│  Readable? YES                           │
└──────────────────────────────────────────┘

The kernel notifies epoll. The main JS thread, sleeping in epoll_wait, wakes again.

§ 21 · libuv Reads the BytesNon-blocking read · hand-off to llhttp

Step by step

▸ libuv-read-flow.txtwake → read → parse
epoll_wait returns
   ↓
"fd 12 is readable"
   ↓
libuv looks up uv_tcp_t for fd 12
   ↓
calls read() syscall
   ↓
kernel copies bytes from receive buffer into a Node-supplied buffer
   ↓
read() returns: number of bytes read
   ↓
libuv invokes the read callback (alloc_cb / read_cb)
   ↓
Node C++ TCPWrap receives the bytes
   ↓
Node C++ feeds bytes to the HTTP parser

Buffer hand-off

▸ buffer-handoff.txtkernel → process → llhttp
┌──────────────────────────────────────────┐
│ Kernel receive buffer                    │
│   "GET / HTTP/1.1\r\nHost: ..."          │
└─────────────────────┬────────────────────┘
                      │ read(fd, dst, n)
                      ▼
┌──────────────────────────────────────────┐
│ Node-allocated Buffer (in process memory)│
│   "GET / HTTP/1.1\r\nHost: ..."          │
└─────────────────────┬────────────────────┘
                      │ delivered to TCPWrap
                      ▼
┌──────────────────────────────────────────┐
│ Node C++ HTTP parser binding (llhttp)    │
└──────────────────────────────────────────┘
!
Lock this in
read() here is not blocking. The kernel was already told that bytes are available, so the call returns immediately with whatever is currently in the receive buffer. If only some of the request has arrived (TCP can deliver in chunks), Node will see read() callbacks multiple times.

§ 22 · The HTTP Parser — llhttpA C state machine that lives inside Node

Modern Node uses llhttp, a parser generated from a state-machine description. It is a C library, accessed via a Node C++ binding. It is incremental:

  • you feed it a chunk of bytes
  • it advances its state machine as far as it can
  • it calls callbacks for each parsed piece: on_message_begin, on_url, on_header_field, on_header_value, on_headers_complete, on_body, on_message_complete

Parser state mid-request

▸ llhttp-state.txtparser state · partial parse
┌────────────────────────────────────────────┐
│ HTTP Parser (llhttp) for fd 12             │
│                                            │
│  current state: PARSING_HEADERS            │
│  parsed so far:                            │
│    method:  GET                            │
│    url:     "/"                            │
│    version: 1.1                            │
│    headers: { Host: "localhost:3000",      │
│               ... }                        │
│  body so far: (none yet)                   │
└────────────────────────────────────────────┘

Arrow flow as bytes feed in

▸ llhttp-feed-flow.txtbyte-by-byte advance
chunk arrives
   ↓
parser.execute(chunk)
   ↓
state machine advances byte-by-byte
   ↓
when it sees "GET ":
   on_url callback prepared
   ↓
when it sees " HTTP/1.1\r\n":
   request line complete
   ↓
when it sees a header:
   on_header_field, on_header_value
   ↓
when it sees the blank line:
   on_headers_complete  ◄── critical!

on_headers_complete is the moment Node decides:

"We have enough to build the JS req/res objects and call the user's request handler."
!
Lock this in
The parser does not need the full body to call your handler. It only needs the headers. Body bytes (if any) continue to arrive afterward and are pushed into the req stream as they come.

§ 23 · IncomingMessage and ServerResponseTwo streams · one socket

When on_headers_complete fires, Node creates two objects:

JavaScript internal · http module
req = new IncomingMessage(socket);
res = new ServerResponse(req);

req is a Readable stream wrapping the inbound side of the socket. res is a Writable stream wrapping the outbound side.

Object relationship

▸ reqres-objects.txtIncomingMessage + ServerResponse · same socket
┌──────────────────────────────────────────┐
│ JavaScript objects for this request      │
│                                          │
│  req (IncomingMessage)                   │
│    - method: "GET"                       │
│    - url: "/"                            │
│    - httpVersion: "1.1"                  │
│    - headers: { host: ..., ... }         │
│    - readable stream of body bytes       │
│    - .socket → net.Socket                │
│                                          │
│  res (ServerResponse)                    │
│    - statusCode: 200 (default)           │
│    - headers (not yet sent)              │
│    - writable stream of body bytes       │
│    - .socket → same net.Socket           │
│                                          │
│         │                                │
│         ▼                                │
│    ┌────────────────────┐                │
│    │ net.Socket (fd 12) │                │
│    └────────────────────┘                │
└──────────────────────────────────────────┘

Arrow flow into JS

▸ reqres-creation.txtheaders complete → JS objects → 'request' event
on_headers_complete
   ↓
Node C++ asks JS: "construct req/res for this connection"
   ↓
http module's parser-onRequest hook runs (JS)
   ↓
new IncomingMessage(socket) created
   ↓
new ServerResponse(req) created
   ↓
server emits 'request' event with (req, res)
   ↓
your handler runs
!
Lock this in
req and res are not real connections. They are JS stream wrappers around the same underlying socket, configured to behave as the read side and write side of a single HTTP exchange.

§ 24 · Your Request Handler Runs on the Main JS ThreadThe moment all that machinery converges

The moment that took us through the kernel, libuv, the HTTP parser, and the http module is the moment your code runs:

JavaScript your handler
(req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello, World!");
}

Call stack at this instant

▸ handler-call-stack.txttop of stack · main JS thread
┌──────────────────────────────────┐
│ your handler (req, res) => {...} │
├──────────────────────────────────┤
│ http server emit('request')      │
├──────────────────────────────────┤
│ http parser onHeadersComplete    │
├──────────────────────────────────┤
│ TCPWrap onread callback          │
├──────────────────────────────────┤
│ libuv I/O callback dispatch      │
└──────────────────────────────────┘

This entire stack is on the main JS thread. The same thread that called server.listen(3000) minutes (or microseconds) ago. The same thread that runs your modules' top-level code. The same thread that runs every other JS callback.

!
Lock this in
There is exactly one thread on which your JavaScript runs. Concurrency in Node doesn't come from threads. It comes from interleaving: many in-flight async operations whose callbacks land on the same single JS thread, one at a time, when the JS stack is empty.

§ 25 · res.writeHead — Building the Status Line and HeadersConfigures · doesn't transmit (yet)

res.writeHead(200, { "Content-Type": "text/plain" }) does not transmit anything yet (necessarily). It records the status code and headers in the res object. Internally, Node prepares a buffer like:

HTTP/1.1 prepared response head
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
Date: ...\r\n
Connection: keep-alive\r\n
Content-Length: 13\r\n
\r\n

(Some of those headers are added automatically.)

Boxed res state after writeHead

▸ res-after-writehead.txtconfigured · not yet flushed
┌──────────────────────────────────────────┐
│ ServerResponse (res)                     │
│                                          │
│  statusCode: 200                         │
│  headers:                                │
│    Content-Type: text/plain              │
│    (Date, Connection added later)        │
│                                          │
│  headersSent: false (not flushed yet)    │
│                                          │
│  pending output buffer:                  │
│    (status line + headers, ready)        │
└──────────────────────────────────────────┘
!
Lock this in
writeHead configures. Bytes don't actually leave Node until something triggers a flush — usually res.write(), res.end(), or a buffer threshold being crossed.

§ 26 · res.end — Flush · Write · Mark CompleteFrom JS string to kernel send buffer

res.end("Hello, World!") does several things:

  1. If headers haven't been flushed yet, flush them now.
  2. Write the body bytes.
  3. Mark the response complete.
  4. Possibly signal the parser/connection that this exchange is done.

Step by step

▸ res-end-flow.txtres.end → uv_write → write() syscall
res.end("Hello, World!")
   ↓
ensure headers flushed
   ↓
combine status-line + headers + body bytes
   ↓
hand the resulting buffer to the underlying net.Socket
   ↓
net.Socket calls C++ TCPWrap.write
   ↓
TCPWrap calls uv_write on the libuv handle
   ↓
libuv calls write() syscall
   ↓
kernel copies bytes into socket fd 12's send buffer
   ↓
write() returns number of bytes accepted
   ↓
   if all accepted: completion will fire soon
   if partial:      libuv arranges to continue when socket writable

Boxed flow into kernel send buffer

▸ write-pipeline.txtJS → process buf → kernel buf → wire
┌──────────────────────────────────────────┐
│ res.end("Hello, World!")                 │
└─────────────────────┬────────────────────┘
                      │ JS → C++
                      ▼
┌──────────────────────────────────────────┐
│ Buffer (process memory)                  │
│                                          │
│  "HTTP/1.1 200 OK\r\n"                   │
│  "Content-Type: text/plain\r\n"          │
│  "Content-Length: 13\r\n"                │
│  "\r\n"                                  │
│  "Hello, World!"                         │
└─────────────────────┬────────────────────┘
                      │ uv_write → write() syscall
                      ▼
┌──────────────────────────────────────────┐
│ Kernel — socket fd 12 send buffer        │
│                                          │
│   bytes queued for transmission          │
└─────────────────────┬────────────────────┘
                      │ TCP layer transmits
                      ▼
┌──────────────────────────────────────────┐
│ Loopback / NIC                           │
└──────────────────────────────────────────┘
!
Lock this in
res.end does not wait for the browser to receive the bytes. It hands the bytes to the kernel. The kernel and TCP take care of delivery. If the socket buffer is full, libuv waits for writability and finishes the send later — without blocking your JS thread.

§ 27 · Backpressure — When the Socket Is Slowlibuv waits for EPOLLOUT · JS keeps running

Suppose the response body is huge and the kernel send buffer fills up.

Step by step

▸ backpressure-flow.txtpartial write → wait for writable → resume
write(fd, buf, n)
   ↓
kernel: only m bytes fit (m < n)
   ↓
kernel returns m
   ↓
libuv records: "still have (n - m) bytes pending"
   ↓
libuv asks epoll: "tell me when fd 12 is WRITABLE"
   ↓
main JS thread continues with other work
   ↓
later: kernel makes room
   ↓
epoll signals fd 12 writable
   ↓
libuv resumes write
   ↓
when all done, calls write_cb
   ↓
res emits 'drain' event in JS

Pending write state

▸ backpressure-state.txtlibuv holds remainder · waits for EPOLLOUT
┌──────────────────────────────────────────┐
│ Pending write request (libuv)            │
│                                          │
│  fd: 12                                  │
│  remaining bytes: 32k of 64k             │
│  on completion: invoke write_cb          │
│                                          │
│  epoll watching: WRITABLE                │
└──────────────────────────────────────────┘
!
Lock this in
The main JS thread never blocks waiting for the socket to drain. libuv uses the same readiness mechanism (epoll/kqueue/IOCP) to be notified when more bytes can be written. This is exactly why streams in Node have .write() returning a boolean and emit 'drain' — to expose this OS-level reality to your code.

§ 28 · The Bytes Flow Back to the BrowserReverse path · symmetric layering

On the browser's machine, the reverse of the inbound path happens.

Step by step

▸ response-back-flow.txtserver → wire → browser
SERVER KERNEL                          BROWSER KERNEL

bytes leave fd 12 send buffer
        │
        ├── TCP transmission
        │
        ▼
                                     bytes arrive
                                            │
                                            ├── delivered to browser's connection socket
                                            ├── receive buffer fills
                                            ├── browser's I/O multiplexer notified
                                            │
                                            └── browser code reads them

Browser's HTTP implementation parses them, and the page or fetch() resolves.

Full round trip

▸ round-trip.txttwo processes · two kernels · one wire
┌─────────────────────────┐               ┌─────────────────────────┐
│ Browser process         │               │ Node process            │
│                         │               │                         │
│ JS:                     │               │ JS:                     │
│  fetch("...3000")       │               │  handler(req, res)      │
│        │                │               │        ▲                │
│        ▼                │               │        │                │
│  HTTP impl              │               │  http module / parser   │
│        │                │               │        ▲                │
│        ▼                │               │        │                │
│  TCP socket fd          │               │  net.Socket fd 12       │
└────────┬────────────────┘               └────────┬────────────────┘
         │                                         │
         ▼                                         ▼
┌─────────────────────────┐               ┌─────────────────────────┐
│ Browser kernel          │               │ Server kernel           │
│  send buffer  ──────────┼─── request ──▶│  recv buffer            │
│  recv buffer  ◀─────────┼─── response ──│  send buffer            │
└─────────────────────────┘               └─────────────────────────┘
              │                                         │
              └────────── network / loopback ───────────┘
!
Lock this in
A complete HTTP request/response is two byte streams flowing in opposite directions through the same TCP connection. Each side's user-space code only deals with its own end. The kernels handle the wire.

§ 29 · Connection Lifecycle After the Responsekeep-alive · close · parser reset

After res.end(), what happens to the TCP connection? It depends on:

  • HTTP version
  • Connection header
  • keep-alive settings

For HTTP/1.1 with Connection: keep-alive (the default)

▸ keep-alive-flow.txtconnection persists · parser resets
- Connection stays open
- Browser may send another request on the same fd
- Server's HTTP parser is reset to wait for the next request
- libuv keeps watching fd 12

For Connection: close

▸ close-flow.txtfd freed both sides
- Server closes the socket after response
- close() syscall
- TCP FIN exchanged
- fd is freed

Parser reset for keep-alive

▸ parser-reset.txtbetween requests on same fd
┌──────────────────────────────────────────┐
│ HTTP Parser for fd 12 (after response)   │
│                                          │
│  state: WAITING_FOR_NEXT_REQUEST         │
│  previous req/res: garbage-collectable   │
│  new req/res will be made on next        │
│  on_headers_complete                     │
└──────────────────────────────────────────┘
!
Lock this in
A single TCP connection may carry many HTTP request/response exchanges sequentially. Each gets its own req/res JS objects. The socket fd, and the kernel-side TCP state, persist across them.

§ 30 · The Critical DiagramEnd-to-end · browser to handler to wire and back

This is the diagram for the entire operation. Memorize it.

▸ critical-diagram.txtbrowser ↔ kernel ↔ libuv ↔ JS
BROWSER PROCESS / KERNEL                NETWORK / LOOPBACK              SERVER KERNEL                       SERVER NODE PROCESS

fetch("http://localhost:3000/")
        │
        ├── DNS lookup
        ├── socket()
        ├── connect()
        │       │
        │       ├── SYN  ─────────────────────────────────────────────▶ kernel processes SYN
        │       │                                                         │
        │       │ ◀──────────────── SYN-ACK ──────────────────────────── │
        │       │                                                         │
        │       └── ACK  ─────────────────────────────────────────────▶ handshake done
        │                                                                 │
        │                                                                 ▼
        │                                                          accept queue: +1
        │                                                                 │
        │                                                          fd 7 → readable
        │                                                                 │
        │                                                          epoll signals ────────────▶ libuv wakes
        │                                                                                          │
        │                                                                                          ├── accept(fd 7) → fd 12
        │                                                                                          ├── new TCPWrap, net.Socket
        │                                                                                          ├── http parser attached
        │                                                                                          ├── uv_read_start(fd 12)
        │                                                                                          │
        ├── format HTTP request bytes                                                               │
        ├── write(fd, "GET / HTTP/1.1...") ─────────────────────▶ recv buffer for fd 12 fills      │
        │                                                                 │                        │
        │                                                          fd 12 → readable                │
        │                                                                 │                        │
        │                                                          epoll signals ────────────▶ libuv wakes
        │                                                                                          │
        │                                                                                          ├── read(fd 12) → bytes
        │                                                                                          ├── feed bytes to llhttp
        │                                                                                          ├── on_headers_complete
        │                                                                                          ├── make req, res
        │                                                                                          │
        │                                                                                          ▼
        │                                                                                  handler(req, res) runs on main JS thread
        │                                                                                          │
        │                                                                                          ├── res.writeHead(200, ...)
        │                                                                                          ├── res.end("Hello, World!")
        │                                                                                          │       │
        │                                                                                          │       ▼
        │                                                                                  uv_write → write(fd 12, response)
        │                                                                                          │
        │                                                          send buffer for fd 12 fills
        │                                                                 │
        │                                                          TCP transmits
        │                                                                 │
        │  ◀────────── HTTP/1.1 200 OK + body ─────────────────────────── │
        │
        ▼
browser recv buffer fills
        │
parses response, fetch() resolves
!
Lock this in
If you can hold this whole picture in your head, you understand a Node HTTP server. The animated 14-step version is what walks through it; this is the static view.

§ 31 · Final ArchitectureTwo machines · all layers · one TCP connection

The end-to-end layered architecture in one boxed diagram. Each box is a layer; arrows show where bytes (and notifications) move.

▸ final-architecture.txttwo machines · all layers
┌────────────────────────────────────────────────────────────────────┐
│ BROWSER MACHINE                                                    │
│                                                                    │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ Browser Process                                              │  │
│  │  - URL parser · HTTP client · TCP socket fd                  │  │
│  └────────────────────────┬─────────────────────────────────────┘  │
│                           │                                        │
│  ┌────────────────────────▼─────────────────────────────────────┐  │
│  │ Browser-side Kernel                                          │  │
│  │  - TCP state machine · send / recv buffers                   │  │
│  └────────────────────────┬─────────────────────────────────────┘  │
└───────────────────────────┼────────────────────────────────────────┘
                            │ network / loopback
                            ▼
┌────────────────────────────────────────────────────────────────────┐
│ SERVER MACHINE                                                     │
│                                                                    │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ Server-side Kernel                                           │  │
│  │  Listening socket fd 7 (LISTEN, accept queue)                │  │
│  │  Connection socket fd 12 (ESTABLISHED, send/recv buffers)    │  │
│  │  epoll/kqueue/IOCP watches both                              │  │
│  └────────────────────────┬─────────────────────────────────────┘  │
│                           │ readiness notifications                │
│                           ▼                                        │
│  ┌──────────────────────────────────────────────────────────────┐  │
│  │ Node Process · Main JS Thread                                │  │
│  │   libuv event loop (poll phase blocks in epoll_wait)         │  │
│  │   Node C++ TCPWrap + HTTPParser (llhttp per connection)      │  │
│  │   Node JS http.Server · net.Socket · IncomingMessage · res   │  │
│  │   V8 executes your handler(req, res)                         │  │
│  │                                                              │  │
│  │ libuv worker pool: NOT involved in HTTP I/O                  │  │
│  └──────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘
!
Lock this in
Eight discrete layers. Bytes flow from top to bottom on the request side, bottom to top on the response side. The kernel is the boundary between application code and physical (or in-memory loopback) transmission.

§ 32 · Step-by-Step Runtime TraceAll 45 steps · in execution order

Every step in the request lifecycle, in the order they actually happen. Each row maps to the thread or kernel component that performs it.

StepThread / Owner
Node process starts, runs server.jsMain JS · V8
http.createServer(handler) creates an http.Server instanceMain JS · V8
server.listen(3000) called on main JS threadMain JS
JS net wrapper invokes Node C++ TCPWrapMain JS · C++
C++ allocates uv_tcp_t handleMain JS · C++
libuv calls socket() → returns fd 7Main JS → kernel
libuv calls bind(fd 7, "0.0.0.0:3000")Main JS → kernel
libuv calls listen(fd 7, backlog)Main JS → kernel
libuv registers fd 7 with epollMain JS → kernel
server.listen returnsMain JS
Main JS thread finishes top-level codeMain JS
Event loop enters poll phase, calls epoll_wait → blocksMain JS BLOCKED
Browser performs DNS lookup for "localhost"Browser
Browser opens TCP socket and calls connect(127.0.0.1:3000)Browser → browser kernel
Kernel handshake: SYN, SYN-ACK, ACKBoth kernels
Server kernel completes handshake, queues connection on accept queueKernel
Server kernel marks fd 7 readable, signals epollKernel
epoll_wait returns on main JS threadMain JS · RUNNABLE
libuv invokes connection I/O callbackMain JS
Node calls accept(fd 7) → returns fd 12Main JS → kernel
Node creates uv_tcp_t for fd 12, TCPWrap, net.SocketMain JS · C++
http.Server attaches HTTP parser (llhttp) to that socketMain JS · C++
Node calls uv_read_start(fd 12), epoll watches fd 12 readableMain JS → kernel
Connection callback finishes, JS stack empties, event loop continuesMain JS
Browser writes HTTP request bytes into its kernel send bufferBrowser → browser kernel
Bytes traverse loopback / network and arrive at server kernelBoth kernels + NIC
Bytes deposited into fd 12's recv bufferKernel
Server kernel marks fd 12 readable, signals epollKernel
epoll_wait returns on main JS threadMain JS
libuv invokes read I/O callback for fd 12Main JS
Node calls read(fd 12) → bytes copied to a Node BufferMain JS → kernel
TCPWrap forwards bytes to HTTPParserMain JS · C++
llhttp state machine advances, parsing request line, headersMain JS · C++
on_headers_complete firesMain JS · C++
http module creates IncomingMessage (req) and ServerResponse (res)Main JS · V8
http.Server emits 'request' event with (req, res)Main JS · V8
Your handler runs on the main JS threadMain JS · V8
res.writeHead(200, ...) records headers in res objectMain JS · V8
res.end("Hello, World!") triggers flushMain JS · V8
TCPWrap calls uv_writeMain JS · C++
libuv calls write(fd 12, buffer)Main JS → kernel
Kernel copies bytes into fd 12 send buffer; TCP transmitsKernel + NIC TX
Browser kernel receives bytes, fills its recv bufferBrowser kernel
Browser HTTP client parses response; fetch() resolves; page rendersBrowser
Server: depending on keep-alive, parser resets or socket closesMain JS
Event loop continues iterating, watching for more eventsMain JS

§ 33 · Thread Ownership MapThree tiers · each owns specific work

The three-tier collapsed view of who owns what. Read top-down: process at the top, kernel in the middle, hardware at the bottom.

▸ ownership-final.txtmain JS · worker pool · kernel
┌──────────────────────────────────────────────────────────────────────────┐
│ NODE PROCESS                                                             │
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │ Main JS Thread                                                     │  │
│  │   - all JS execution                                               │  │
│  │   - http.Server / net.Server / Socket JS objects                   │  │
│  │   - C++ TCPWrap / StreamWrap / HTTPParser                          │  │
│  │   - libuv event loop (poll phase calls epoll_wait)                 │  │
│  │   - issues socket/bind/listen/accept/read/write syscalls           │  │
│  │   - feeds bytes to llhttp · creates req/res · runs your handler    │  │
│  └────────────────────────────────────────────────────────────────────┘  │
│                                                                          │
│  ┌────────────────────────────────────────────────────────────────────┐  │
│  │ Libuv Worker Thread Pool                                           │  │
│  │   * NOT used for HTTP request/response *                           │  │
│  │   (only relevant for: DNS getaddrinfo, crypto, zlib, fs)           │  │
│  └────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ OS KERNEL                                                                │
│   epoll instance · TCP/IP stack · listen+accept queues                   │
│   per-socket recv & send buffers · TCP state machines · timers           │
│   network softirqs · scheduler                                           │
└──────────────────────────────────────────────────────────────────────────┘
                                   │
                                   ▼
┌──────────────────────────────────────────────────────────────────────────┐
│ HARDWARE                                                                 │
│   NIC (RX/TX ring buffers, DMA, interrupts) · physical link              │
└──────────────────────────────────────────────────────────────────────────┘
!
Lock this in
Each tier owns a precise set of responsibilities. The boundaries are crisp: JS never touches kernel state directly, the kernel never speaks HTTP, the hardware doesn't know what TCP is.

§ 34 · Is the Worker Pool Involved?No · network I/O uses readiness, not worker threads

Network I/O in Node is not worker-pool-based. The libuv worker pool exists, but it's not what carries HTTP requests. Compare with fs.readFile from Part 5:

▸ fs-vs-tcp.txttwo strategies
┌──────────────────────────────────────────┐
│ fs.readFile                              │
│                                          │
│  Strategy: delegate blocking work        │
│                                          │
│  main JS thread                          │
│    └─ submit job                         │
│         └─ worker thread                 │
│              └─ open/read/close          │
│                   └─ may block in kernel │
│                        └─ completion     │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│ http server                              │
│                                          │
│  Strategy: readiness multiplexing        │
│                                          │
│  main JS thread                          │
│    └─ register fd with epoll             │
│         └─ epoll_wait                    │
│              └─ kernel signals readiness │
│                   └─ Node reads bytes    │
│                        └─ parser → JS    │
│                                          │
│  No worker thread used.                  │
└──────────────────────────────────────────┘
!
Lock this in
Workers are for operations that the OS cannot watch with a readiness API or that are CPU-bound. Sockets are exactly the case the readiness API was designed for.

§ 35 · Are epoll, kqueue, or IOCP Involved?Yes · every step that says "Node is waiting for the network" is a call into one of these

Whichever OS you are on, the pattern is the same from Node's perspective: "I told the OS what fds I care about. The OS told me when something happened. Then I responded."

▸ os-readiness-mechanisms.txtlinux · macos · windows
┌──────────────────────────────────────────┐
│ Linux                                    │
│                                          │
│ epoll_wait blocks main JS thread         │
│ kernel marks fds ready                   │
│ thread wakes, reads each ready fd        │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│ macOS / BSD                              │
│                                          │
│ kevent blocks main JS thread             │
│ kernel marks events ready                │
│ thread wakes, processes each event       │
└──────────────────────────────────────────┘

┌──────────────────────────────────────────┐
│ Windows                                  │
│                                          │
│ Node submits overlapped operations       │
│ Kernel completes them                    │
│ Completion port wakes thread             │
│ thread processes completions             │
└──────────────────────────────────────────┘
!
Lock this in
libuv hides the differences between Linux, macOS/BSD, and Windows behind one event-loop abstraction. From Node's perspective: register interest, sleep, react.

§ 36 · Can the Main JS Thread Be Blocked?Yes · but not by the network operations themselves

The main JS thread can still be blocked by:

  • CPU-heavy JavaScript inside your handler
  • synchronous fs APIs (fs.readFileSync, etc.)
  • huge JSON.parse / JSON.stringify
  • regex catastrophic backtracking
  • infinite loops
  • blocking native addons
▸ cpu-bound-timeline.txtqueued requests waiting
Time ───────────────────────────────────────────────────►

Main JS thread:
[handler request A: huge loop ............................] [handler B] ...

Network requests B, C, D arrive:
                  [accepted by kernel, queued in recv buffers]
                  [their callbacks WAITING for main thread...]

epoll has events ready, but the JS stack is busy.
!
Lock this in
The kernel will happily continue accepting connections and buffering bytes. But your JS handler is the bottleneck. If it doesn't release the thread, Node's apparent concurrency disappears — every other client waits.

§ 37 · Wrong Mental Model vs CorrectEight common takes — and what's actually true

Wrong
Node spawns a thread per request.
Node uses ONE main JS thread for all requests. The OS readiness API lets that one thread handle thousands of sockets.
Wrong
Network I/O uses the libuv worker pool.
The worker pool is for fs, crypto, dns.lookup, etc. Network I/O uses epoll/kqueue/IOCP directly from the main JS thread.
Wrong
accept() blocks waiting for connections.
By the time Node calls accept(), the kernel has already completed the handshake and queued the connection. accept() returns immediately. The waiting was done by epoll_wait (or equivalent), not by accept itself, and even epoll_wait wakes the moment something is ready.
Wrong
req and res are different connections.
req and res are JS stream wrappers over the SAME underlying TCP socket. req is the readable side; res is the writable side; both reference one socket.
Wrong
res.end() guarantees the browser has received the data.
res.end() hands the bytes to the kernel send buffer. TCP delivers them, eventually. Node has no direct confirmation of browser receipt at the application layer.
Wrong
The HTTP parser is part of the OS or browser.
The HTTP parser is a Node-included library (llhttp) that runs in your Node process. The OS only knows about TCP bytes. HTTP is interpreted entirely in user space.
Wrong
server.listen blocks until a connection arrives.
server.listen returns almost immediately. It just sets up the listening socket and registers it with epoll. The 'request' callback runs much later, when actual connections occur.
Wrong
The handler runs in parallel for multiple requests.
Handlers run sequentially on the single main JS thread. What is parallel is the kernel's handling of TCP for many sockets. JS handlers interleave: one runs, returns or awaits, another can then run. They don't truly execute simultaneously.

Correction summary

▸ wrong-vs-right-summary.txtside-by-side
┌────────────────────────────────┬───────────────────────────────────────┐
│ Wrong                          │ Correct                               │
├────────────────────────────────┼───────────────────────────────────────┤
│ thread per request             │ one main JS thread, kernel concurrency│
│ network uses worker pool       │ network uses epoll/kqueue/IOCP        │
│ accept() blocks                │ accept() returns from queued conn     │
│ req and res are separate conns │ they wrap the same socket             │
│ res.end means delivered        │ res.end means handed to kernel        │
│ parser is in OS                │ parser is llhttp inside Node          │
│ listen() blocks                │ listen() returns immediately          │
│ handlers run in parallel       │ handlers run interleaved on one thread│
└────────────────────────────────┴───────────────────────────────────────┘

§ 38 · The Compressed End-to-End SummaryFive stages · the whole story

The story compressed to its essential five stages. Each stage hides several syscalls and a kernel state transition, but if you can recite these five lines you have the model.

▸ compressed-summary.txt5 stages · the whole story
┌──────────────────────────────────────────┐
│ 1. listen()  → kernel listening socket   │
│              → epoll watches it          │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ 2. browser handshake → kernel accepts    │
│    epoll signals → libuv accept()        │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ 3. browser sends request → kernel buffer │
│    epoll signals → libuv read()          │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ 4. llhttp parses → req, res created      │
│    'request' event → your handler runs   │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ 5. res.end → uv_write → kernel send buf  │
│    TCP transmits → browser receives      │
└──────────────────────────────────────────┘

§ 39 · Two Machines · Three Worlds EachJS · native · OS — on each side

The three-world model applies to both sides of the conversation. The browser machine and the server machine each have their own JS world, native world, and OS world. Bytes only travel between them at the OS layer.

Browser machine

▸ browser-three-worlds.txtJS · native · OS
┌──────────────────────────────────────────┐
│ Browser JS / app world                   │
│                                          │
│ - fetch / XHR / navigation               │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Browser native HTTP / TCP world          │
│                                          │
│ - HTTP impl                              │
│ - TLS impl                               │
│ - connection pool                        │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Browser OS / kernel / hardware world     │
│                                          │
│ - sockets                                │
│ - TCP / IP                               │
│ - NIC / loopback                         │
└──────────────────────────────────────────┘

Server machine

▸ server-three-worlds.txtJS · libuv · kernel
┌──────────────────────────────────────────┐
│ Server JS world                          │
│                                          │
│ - your handler                           │
│ - req, res                               │
│ - http.Server                            │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Server Node native / libuv world         │
│                                          │
│ - TCPWrap, llhttp                        │
│ - libuv handles                          │
│ - event loop, epoll/kqueue/IOCP glue     │
└─────────────────────┬────────────────────┘
                      ▼
┌──────────────────────────────────────────┐
│ Server OS / kernel / hardware world      │
│                                          │
│ - listening socket                       │
│ - per-connection sockets                 │
│ - send / receive buffers                 │
│ - TCP / IP / NIC / loopback              │
└──────────────────────────────────────────┘

Between them: bytes on a wire, or memory in a loopback path.

!
Lock this in
An HTTP request is a coordinated dance between two processes' user-space code, two kernels, and the wire between them. Node only owns one slice — the server-side user-space. Everything else, including the heavy lifting of moving bytes and tracking thousands of TCP states, belongs to the kernel.

§ 40 · The Full Invariant — Final Lock-this-inTen points · the whole model in a list

If you forget every diagram in this lesson, remember these ten lines. Each one collapses a section of the trace down to a single invariant.

▸ The full invariant
  1. The kernel owns and drives every TCP connection.
  2. libuv asks the kernel — via epoll/kqueue/IOCP — to be notified when any registered socket needs attention.
  3. When a notification arrives, the main JS thread reads bytes (or accepts a new connection).
  4. It feeds those bytes to llhttp.
  5. It builds req/res JS objects.
  6. It runs your handler.
  7. It writes response bytes back to the kernel.
  8. It returns to the event loop and sleeps in epoll_wait again.
  9. There is no request-per-thread. There is no spin-waiting. There is no busy polling.
  10. One thread, one event loop, and the kernel doing the heavy lifting.
!
Final lock-this-in
A Node HTTP server is fundamentally a TCP server with an HTTP parser bolted on top. server.listen(3000) registers and returns. req and res are stream wrappers over the same socket. Your handler runs on the main JS thread. res.end() hands bytes to the kernel — TCP and the kernel are responsible for delivery. That is the full browser HTTP request → Node server response story.
Back to JIT-Atlas