TT Lab
Blog Get started
Learn Learning paths Courses

Node.js Backend — What the Framework Hides

A Socket Server's Defaults Are Not Production Settings

Continue in TT Lab

In one line

The defaults of http.createServer are tuned for "it starts up fine locally". A production server that must withstand slow clients, large bodies, and deployment signals behind a front end (a load balancer) has to set five of those defaults itself — body size, header and request time limits, keep-alive time, error responses, and shutdown order.

Why this was needed

The handler in the previous module was a function that returns {status, body}. That is where the contract is defined. But the moment you attach that function to a socket, things outside the contract happen. A client sends its headers one line per second. A request comes in declaring a 10MB body. The front end sends a request over a connection that has sat idle for a minute. The deployment tool sends SIGTERM. These four never show up in handler tests, and they show up every day in production.

Measuring Node 22's defaults gives this (measured 2026-11-05 on Node 22.11): keepAliveTimeout 5 seconds, headersTimeout 60 seconds, requestTimeout 300 seconds, and no body size limit. And the time limit check runs once every connectionsCheckingInterval (default 30 seconds) — even with headersTimeout set to 3 seconds, a server that did not shorten the check interval had not cut a slow connection after 10 seconds. If you only change the value and do not measure, you think you fixed it.

How it works

Mechanism What it prevents Where you set it
Body limit Exhausting memory and time with a large body. Reject early on the Content-Length header, and count while receiving chunked bodies that do not declare their size The handler (yourself)
headersTimeout Connections that drag out their headers (slowloris) createServer option
requestTimeout Connections that drag out their body. Does not include the time to produce the response createServer option
keepAliveTimeout The "send on a closing connection" race that happens when the server closes before the front end server property
Error response The whole process dying (a thrown error nobody catches), or stacks and secrets going out in the response The catch of the request handler

With keep-alive, direction matters. The front end pools connections to the server and reuses them. If the server closes a connection after 5 seconds while the front end believes it can use it for 60 seconds, a request the front end sends at the very moment the server closes is cut off, and the user sees a 502. So the side that closes must be the front end, and the server's keepAliveTimeout must be longer than the front end's idle time.

Shutdown is an order. When it receives SIGTERM, it (1) stops accepting new connections (server.close()), (2) closes idle connections, (3) answers in-flight requests to completion but closes those connections after the response (Connection: close), and (4) exits by itself with 0 when everything is done. (5) If it passes the deadline, it cuts the remaining connections — by itself, before the orchestrator's SIGKILL arrives. There is a trap here. server.close() closes only the connections idle at that moment. A keep-alive connection that was working then stays open even after finishing its response, so the close callback never comes, and the front end keeps sending requests over that connection. In measurement, such a process stayed alive until it was SIGKILLed.

What it looks like in the field

The common report "a few 502s happen only during deploys" is usually one of three. Code that calls process.exit right away on SIGTERM (in-flight requests are cut off), a graceful shutdown with too short a deadline (shorter than the longest request), and the open keep-alive connection above (it never ends, then SIGKILL). All three look the same in the logs as one "502" line, and the clue that tells them apart is when and how the old process ended after SIGTERM. In Kubernetes, endpoint removal and SIGTERM start at the same time, so you add a preStop hook that holds on for a few seconds, but that is outside the scope of this lab.

You meet body and time limits through accidents before attacks. A retry bug in a mobile app keeps resending the same 10MB upload and server memory swings, or clients in an area with a bad network leave hundreds of half-open connections and sockets run dry. At that point, "the framework will block it for me" is usually wrong — Express's body-parser has a default 100KB limit, but the standard http has nothing, and for time limits both inherit Node's defaults as they are.

Error responses are the same. A server where a single throw kills the whole process; a server where, to prevent that, unhandledRejection was changed to only log, and now requests never get an answer (the incident of the previous module); and a server that put err.stack as is into a 500 response and revealed the DB password — all of them really exist.

What you do in the next lab

You grow a single /root/work/srv/server.mjs over six steps. The grader starts that file itself on a free port and pokes it with a real socket — it sends headers one line every 0.5 seconds, declares 10MB and sends only 1KB, leaves a connection idle for 7 seconds and then reuses it, and sends SIGTERM in the middle of work. The last step is an incident. Three services produce 502s on every deploy, and the cause is different for each service.