So a client opens a TCP connection to your Node server. Walk me through what actually happens between that moment and your request handler running.
A strong answer follows one request from the accepted socket through llhttp parsing into the JS handler and can say which layer owns what.
What an AI-prepared candidate might say
So the client connects, and Node accepts the TCP connection and starts reading bytes off the socket. It parses the HTTP request line, the headers, the body, and builds a request object and a response object out of that, and those get handed to your handler as req and res. You pull whatever you need off req, the URL, the method, headers, maybe the body, do your work, and write the response back with res.write and res.end. Node sends those bytes back over the same socket. And the event loop is basically how one thread handles a lot of connections at once, while one request is waiting on I/O, Node just works on the others. The http module wires most of this up for you, you register a callback with createServer and Node does the accepting and parsing and dispatching.
Senior
It's really a handoff chain across three layers. The kernel finishes the TCP handshake on its own and parks the connection on the listen socket's accept queue. libuv is watching that listen fd through epoll or kqueue, it accepts the connection and wraps it in a net.Socket. At this point no HTTP has happened at all. What you've got is a duplex byte stream, that's it.
Then bytes start arriving, the socket emits data, and Node feeds each chunk into llhttp, which is a C parser compiled from a state machine. And the thing about llhttp is it's incremental. It doesn't sit around waiting for a complete request, it fires callbacks as it crosses boundaries, headers complete, each body chunk, message complete. When the headers finish, Node builds the IncomingMessage and ServerResponse, that's your req and res, and calls your handler. The timing detail most people miss is right here. Your handler runs after the headers are parsed but often before the body has fully arrived. req is a readable stream the parser is still actively feeding, and res is a writable you can start writing to immediately.
Your writes go back through the socket into the kernel send buffer. On keep-alive the socket stays open and llhttp just resets its state for the next request on the same fd. All of this parks in the event loop's poll phase. An idle socket costs a file descriptor and a little kernel memory, it doesn't hold a thread, which is how one process sits on tens of thousands of connections. Once you can name the layers you can predict where a failure is going to surface, accept queue, parser, or handler.
Staff
Under load every stage in that chain fails differently, and the job is knowing which counter moves. Take the accept queue. If completed handshakes arrive faster than your loop accepts them, and that queue is bounded by the listen backlog, 511 by default, capped by somaxconn, the kernel starts dropping or resetting new connections. Clients see connect timeouts while your CPU graph looks idle. Nothing looks wrong at the network layer, the loop is just too busy to drain the queue. I catch that by watching event-loop delay from monitorEventLoopDelay next to the OS listen-overflow counter.
The parser has its own guardrails and I don't turn them off casually. server.headersTimeout is 60s by default, server.requestTimeout is 300s. They exist so a client dribbling bytes can't pin a parser and a socket open forever. I've watched someone disable them to work around a proxy quirk, and that's a slowloris hole reopened, plain and simple.
And the tradeoff I'll actually argue for is keeping the handler's synchronous stretch tiny. Anything CPU-heavy between the parser handing you req and your first await stalls every other connection parked in the poll phase. p99 climbs, median stays flat, and everyone stares at the wrong graph. So I track event-loop lag instead of average response time, and I keep body parsing streaming rather than buffering whole payloads into memory. When latency gets ambiguous I timestamp at accept, at headers-complete, and at handler entry. Then I can say which layer owns the delay instead of guessing.
Follow-up chain
- So where does backpressure kick in on this path if a client's uploading a big body faster than your handler reads it?
- And what's actually happening to the TCP connection while you're not reading req?
- Okay, keep-alive means a second request comes down that same socket. What has to get reset between the two?
- Say latency is creeping in somewhere. How do you prove it's the parser adding it and not your handler?