Node.js HTTP & Networking

Node.js HTTP & Networking

Overview

This reference is the networking + HTTP core of Node plus the modern HTTP client: the layered stack from raw TCP/UDP sockets up through HTTP/1.1, HTTP/2, TLS, and the fetch/undici client. It is the “talk to the network correctly and keep the sockets healthy” companion to three siblings that own neighbouring layers:

The mental model has four layers: net/dgram (TCP/UDP sockets) → tls (encryption, SNI, ALPN) → http / http2 / https (framing) → fetch/undici (the high-level pooled, retrying client). Most production incidents here are timeout and socket-pool problems, not protocol problems — so the timeout knobs and Agent/Pool sizing get the most attention below.

Core concepts

1. node:http — server lifecycle, IncomingMessage/ServerResponse, request & Agent

http.createServer([options][, requestListener]) returns an http.Server. The lifecycle is event-driven, and the events you actually wire up are:

Client side: http.request(options|url[, callback]) returns a writable ClientRequest; http.get is the same but auto-end()s and is GET-only. Key options: hostname/host, port (default 80), method (default GET), path, headers, agent, timeout. Header size is capped by --max-http-header-size (default 16 KiB), readable as http.maxHeaderSize.

The http.Agent manages the socket pool for outbound requests (the default is http.globalAgent, which historically has keepAlive: false). Construct your own to reuse connections. Options and defaults:

Option Default Meaning
keepAlive false Reuse sockets across requests (set true in production clients).
keepAliveMsecs 1000 Initial delay for TCP keep-alive probes on kept sockets.
maxSockets Infinity Max concurrent sockets per origin. Infinity is a footgun — bound it.
maxFreeSockets 256 Max idle kept-alive sockets per origin.
maxTotalSockets Infinity Max sockets across all origins.
scheduling 'lifo' 'lifo' reuses the hottest socket (better for keep-alive expiry); 'fifo' round-robins. Default became 'lifo' in v15.6.

2. node:http — server & socket timeouts (the anti-slowloris knobs)

These four properties are the most operationally important thing in the module. Misconfigured, they cause hung requests, leaked sockets, and the infamous 502 behind a load balancer:

3. node:http2 — secure/insecure servers, sessions, streams, ALPN, compatibility API

4. node:https + node:tls — secure context, SNI, ALPN, session resumption

node:https is HTTP semantics carried over node:tls: https.createServer(options, listener) and https.request take the same shape as their http counterparts plus TLS options. There is a dedicated https.Agent, which additionally keeps a client-side TLS session cache (keyed by host) so reconnections can resume the TLS session and skip a round trip — a meaningful win for a keep-alive-light, many-origins client (maxCachedSessions bounds it).

The real depth is node:tls:

5. node:net — the TCP connection model, allowHalfOpen, Nagle, keep-alive

node:net is the TCP/IPC layer everything above sits on. net.createServer([opts][, listener]) emits 'connection' (socket); net.connect/net.createConnection open a client net.Socket (a Duplex stream emitting 'data', 'end', 'close', 'error', 'timeout', 'ready'). The socket controls you reach for:

6. node:dgram — UDP sockets (brief)

Connectionless UDP. dgram.createSocket('udp4'|'udp6') → a socket you bind([port]) and read via the 'message' (msg, rinfo) event; socket.send(msg, port, address) to transmit (no connection, no delivery guarantee). socket.connect(port, address) pins a default remote so you can send(msg) without re-specifying it. Multicast: addMembership/dropMembership, setMulticastTTL, setMulticastLoopback; broadcast: setBroadcast(true). Used for DNS, mDNS/SSDP discovery, metrics (StatsD), and as the substrate under QUIC/HTTP-3.

7. The global fetch is undici — Dispatcher, Client, Pool, Agent

Node’s global fetch/Request/Response/Headers (stable since v21) is implemented by undici, Node’s from-scratch HTTP/1.1 client. Understanding undici is understanding fetch’s performance.

8. undici keep-alive & timeout options (the client-side mirror of §2)

Client/Pool constructor options and their current defaults (verify against your undici version — these changed historically):

Tools & frameworks

Tool / API What it is When to reach for it
node:http / http.Server Core HTTP/1.1 server + client; the http.Agent socket pool. Any HTTP/1.1 work; the base under every framework.
node:http2 Multiplexed HTTP/2 (h2/h2c), compatibility API. gRPC-style multiplexing, many small assets, h2 from browsers.
node:tls / node:https TLS plumbing — SNI, ALPN, session resumption — and HTTP-over-TLS. Terminating TLS in-process, multi-cert hosting, ALPN negotiation.
node:net Raw TCP / IPC sockets. Custom wire protocols, proxies, low-latency setNoDelay paths.
node:dgram UDP datagram sockets, multicast. DNS, discovery (mDNS/SSDP), StatsD metrics, QUIC substrate.
global fetch / undici WHATWG fetch (= undici) and the Client/Pool/Agent client. Outbound HTTP from a Node service; pooled, retrying, proxied clients.
undici.MockAgent In-process network mocking via setGlobalDispatcher. Unit-testing code that calls fetch/undici without real sockets.

Methodology / practical patterns

  1. Always set client keep-alive. A bare http.request with the default globalAgent (keepAlive: false) opens and tears down a TCP+TLS connection per request. Use a shared new http.Agent({ keepAlive: true, maxSockets: <bounded> }), or for fetch call setGlobalDispatcher(new Agent({ connections: N })) once at startup.
  2. Order the timeout sandwich correctly: Node server.keepAliveTimeout > upstream LB idle timeout, and give headersTimeout/requestTimeout finite values so a stuck client can’t pin a socket forever. Mirror it on the client with undici headersTimeout/bodyTimeout.
  3. Bound maxSockets/connections. Infinity (the default) means a downstream slowdown lets pending requests open unbounded sockets → fd exhaustion. Size the pool to the downstream’s capacity.
  4. Pick the protocol deliberately: HTTP/2 (createSecureServer + ALPN 'h2') for many concurrent streams to one origin; HTTP/1.1 + a Pool of connections when the server isn’t h2. Don’t enable HTTP/1.1 pipelining on the open internet.
  5. Reuse a SecureContext across connections instead of re-reading PEM per request; enable session resumption (tickets + shared ticketKeys behind an LB) to cut handshake round-trips.
  6. Test with MockAgent, not a live network: const mock = new MockAgent(); setGlobalDispatcher(mock); mock.get(origin).intercept({ path }).reply(200, body).

Anti-patterns

Troubleshooting

References