Backend Node.js Course · Level 2
HTTP in Node.js: request, response, keep-alive
Understand IncomingMessage, ServerResponse, status codes, headers, and keep-alive - the layer under Express or Fastify.
On this page
Every Node framework (Express, Fastify, Nest, Hono) sits on node:http. The server receives an IncomingMessage (request) and a ServerResponse (response). Frameworks add routing, middleware, and helpers - they do not replace HTTP.
1. Response: res.writeHead() + res.write()/res.end()
- The native
httpmodule only provides low-level building blocks:http.createServer((req, res) => {...}), without built-in.json(),.status(), or automatic routing. - Writing a response manually (without Express):
js
res.writeHead(200, { 'Content-Type': 'application/json' });
res.write(JSON.stringify({ users: ['alice', 'bob'] }));
res.end();
- Express's
res.status(200).json(data)is essentially a cleaner wrapper around those exact 3 steps: setting the status code, setting theContent-Typeheader, runningJSON.stringify(), and callingend().
2. Request: req is a Readable stream
reqin the nativehttpmodule is actually a Readable stream; body data arrives in chunks via thedataevent and completes with theendevent, rather than being available as a complete block from the start. (The general concept of Readable streams was covered in Phase 1 viafs.createReadStream, while recognizing that an HTTP server'sreqis also a Readable stream is the new connection learned here.)- Writing manually to receive the full body before processing:
js
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
const data = JSON.parse(body);
// only now is the full data available to use
});
express.json()(middleware) essentially performs that exact process: listening fordata/end, aggregating chunks, runningJSON.parse(), and assigning the result toreq.body.- Because reading the entire body stream is an asynchronous operation,
express.json()must run as middleware before the route handler (callingnext()once finished), ensuring that by the time the handler runs,req.bodyis fully populated. - Practical consequence: forgetting
app.use(express.json())causesreq.bodyin route handlers to always beundefined, because no middleware consumed the stream.
3. Connection keep-alive
- Before sending an HTTP request, the client and server must perform a TCP three-way handshake, which adds latency (ranging from tens to hundreds of milliseconds depending on network distance).
Connection: keep-aliveinstructs the client and server: "do not close the TCP connection after sending the response; keep it open for subsequent requests," preventing repeated handshakes when loading multiple resources from the same server (for example, 20 HTML/CSS/JS/image files for a webpage).Keep-Alive: timeout=5(observable in actual response headers) is a balanced configuration: long enough to reuse connections for closely timed requests, but automatically closing after 5 seconds of inactivity to avoid holding connection resources indefinitely.- Express (via the underlying
httpmodule) enables keep-alive automatically by default without requiring manual configuration. - Trade-off: keeping connections open saves handshake overhead for subsequent requests, but it consumes system resources (file descriptors, RAM) while sitting idle waiting for the next request, which impacts the total number of concurrent connections the server can handle.
4. Keep-alive risks in production and protective timeouts
- Slow clients occupying sockets: A client on a weak network or sending a large body (slow upload) keeps the TCP connection open for a long time, occupying 1 slot in the server's concurrent connection capacity, even though the server is doing nothing during that time except waiting for data.
- Node's built-in protective mechanisms:
server.maxConnections: a hard limit on the number of concurrent connections, rejecting new connections once exceeded.headersTimeout(Node 18+): the maximum time limit to receive complete HTTP headers, preventing clients from trickling headers (Slowloris-style attacks).requestTimeout(Node 18+): the maximum time limit for the entire request, automatically terminating if the handler takes too long to process or the client sends the body too slowly.
- Load balancers have their own idle timeouts: A Node server typically sits behind a load balancer (Nginx, AWS ALB...). If Node keeps a connection open longer than the load balancer's timeout, the load balancer silently closes the connection on its end while Node assumes it is still active, causing subsequent requests on that connection to fail abruptly with
ECONNRESETwithout an obvious cause (an "opaque disconnect"). Node's timeout must be configured to be shorter than that of the upstream load balancer. - Interview takeaway: when a Node server is "slow" in production, the root cause is often not a blocked event loop, but rather a request handler that never calls
res.end()(hanging indefinitely), the absence of protective timeouts, orawaiting an unbounded I/O operation (e.g. a hanging third-party API call). These "zombie" requests silently monopolize connections and exhaust the server's capacity to serve traffic, even while CPU and event loop usage remain idle. - Practical takeaway: the missing
next()error practiced in the Middleware section (which causes the response to hang indefinitely because it never reachesres.end()) is a concrete, easily visible example at the development level of the exact same "handler never ends" error class that production encounters at scale.
