Phase 1
How Node's HTTP Server Actually Handles a Request
What http.createServer really does underneath, how keep-alive connections work, and what a framework like Express adds on top.
Interactive example
From TCP connection to response
Step through what happens between a socket connecting and the response going out.
Step 1 of 6
TCP connection accepted
libuv accepts the incoming connection and hands Node a raw socket, wrapped as a net.Socket.
createServer() is not magic, it's a TCP listener with a parser attached
const http = require("http");
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("hello");
});
server.listen(3000);http.createServer() returns an http.Server, which under the hood is a net.Server - a raw TCP listener - with an HTTP parser wired into every socketSocketThe live, two-way connection between one specific client and a server. A port routes traffic to a process; a socket is the individual, ongoing conversation happening on top of that port. it accepts. .listen(3000) asks libuv to bind that port and start accepting connections, exactly the same libuv machinery covered in the event loopEvent loopThe mechanism that lets a single-threaded runtime like Node.js handle many operations at once by never sitting idle - it starts slow work, moves on, and comes back when a result is ready. and async I/O topics. Nothing here is HTTP-specific at the socket level; the parsing is what makes it HTTP.
From accepted socket to your callback
When a TCP connection comes in, libuv hands Node's C++ layer a new socket. From there:
- The socket is wrapped in a JavaScript
net.Socket - Raw bytes arriving on that socket get fed into an HTTP parser (
llhttp, built into Node) as they arrive - The parser reads the request line and headers incrementally, byte by byte, off the wire
- Once the headers are fully parsed, Node constructs an
http.IncomingMessage(this is yourreq) and anhttp.ServerResponse(res) - Your callback runs, with
reqandreshanded to it
Nothing about this waits for the full request body to arrive before your callback fires - only the request line and headers need to be parsed. req is itself a readable stream; the body arrives as data events (or via await on an async iterator) exactly like any other readable stream, which is why a large file upload doesn't need to sit fully in memory before your handler starts.
Keep-alive: one socket, many requests
HTTP/1.1 connections default to keep-alive - the TCP socket stays open after a response finishes, so the next request from the same client reuses it instead of paying for a new TCP handshake (and TLS handshakeTLS handshakeThe exchange that happens right after a connection opens, where a client and server agree on encryption keys before any real data is sent - the reason HTTPS costs a bit more time upfront than plain HTTP., if HTTPS) every time.
const server = http.createServer((req, res) => {
res.end("ok");
});
server.keepAliveTimeout = 5000; // close idle sockets after 5s of no new request
server.headersTimeout = 60000; // max time allowed to receive headers on a socketkeepAliveTimeout is the setting that actually matters in production, and it's the source of a specific, well-known bug: if a load balancerLoad balancerA component that sits in front of multiple servers and distributes incoming requests across them, so no single machine gets overwhelmed and a crashed instance doesn't take the whole system down. or reverse proxy in front of Node has a keep-alive timeout longer than Node's own, Node can close a socket the proxy still thinks is alive, and the next request routed onto it fails with a connection reset. The fix is keeping Node's keepAliveTimeout slightly higher than whatever's in front of it, not lower.
Backpressure applies to res the same way it applies to any writable stream
res is a writable stream, so everything from the streams topic applies directly. res.write(chunk) returns false when the underlying socket's buffer is full - meaning the client (or the network path to it) is consuming data more slowly than your server is producing it.
const readStream = fs.createReadStream("./large-report.csv");
readStream.pipe(res); // pipe() respects res's backpressure automatically// manual write - backpressure has to be handled explicitly
function sendChunks(res, chunks, i = 0) {
if (i >= chunks.length) return res.end();
const ok = res.write(chunks[i]);
if (ok) {
sendChunks(res, chunks, i + 1);
} else {
res.once("drain", () => sendChunks(res, chunks, i + 1));
}
}Ignore the return value of res.write() in a loop and you can buffer an unbounded amount of data in memory waiting for a slow client - the same failure mode as ignoring backpressureBackpressureWhat happens when a fast producer sends data faster than a slow consumer can process it. Without a mechanism to signal 'slow down,' the unconsumed data piles up in memory until something breaks. on any other stream, just triggered by a slow network peer instead of a slow disk.
What Express actually adds
The raw http module gives you exactly one callback per request, a stream to read from, and a stream to write to - no routing, no parsed body, no concept of middlewareMiddlewareCode that runs between a request arriving and your actual logic handling it - checking auth, logging, parsing the body. A pipeline of small steps a request passes through before reaching its final handler.. Express (and frameworks like it) sits on top of that same http.createServer(), adding:
- Routing - matching
req.methodandreq.urlagainst registered paths, instead of every request funneling into one callback and being dispatched by hand - Middleware chaining - a
next()-based pipeline so multiple functions can run in sequence per request, each able to short-circuit or pass control forward - Body parsing -
express.json()reads and buffers thereqstream and gives you a parsedreq.body, work you'd otherwise write yourself against the raw stream - Response helpers -
res.json(),res.status(),res.send()wrap the same underlyingres.writeHead()/res.end()calls in something more convenient
None of this replaces the http module - req and res inside an Express handler are still, at their core, the same IncomingMessage and ServerResponse objects http.createServer would have handed you directly, just with extra properties and methods attached. Express is a layer of ergonomics over the same request lifecycle, not a different mechanism underneath it.