Node.js HTTP & Networking
Parent: JavaScript and Node.js · researched 2026-06-02T18:02:34.004Z· 10 sources · 8 concepts · skill nodejs-http-networking
This reference is the networking + HTTP core of Node plus the modern HTTP client:
Overview
- This reference is the networking + HTTP core of Node plus the modern HTTP client: [source]
- the layered stack from raw TCP/UDP sockets up through HTTP/1.1, HTTP/2, TLS, and the [source]
- fetch/undici client. It is the "talk to the network correctly and keep the sockets [source]
- healthy" companion to three siblings that own neighbouring layers: [source]
- nodejs-backend-frameworks owns the framework layer (Express/Fastify/NestJS/Hono, [source]
- routing, middleware, framework selection). This file is the primitives those frameworks [source]
- are built on - http.Server, the Agent, timeouts, TLS. [source]
- nodejs-concurrency-internals owns the libuv event-loop phase model and **stream [source]
- backpressure** (highWaterMark, pipe vs pipeline flow control). This file uses [source]
- streams (request/response bodies are streams) but defers the backpressure mechanics there. [source]
- http-security-headers owns CSP/HSTS/CORS and mTLS hardening posture. This file [source]
- covers the TLS plumbing (SNI, ALPN, session resumption); the security headers go there. [source]
- The mental model has four layers: net/dgram (TCP/UDP sockets) → tls (encryption, [source]
- SNI, ALPN) → http / http2 / https (framing) → fetch/undici (the high-level pooled, [source]
- retrying client). Most production incidents here are timeout and socket-pool problems, [source]
- not protocol problems - so the timeout knobs and Agent/Pool sizing get the most attention below. [source]
1. `node:http` — server lifecycle, IncomingMessage/ServerResponse, request & Agent
- http.createServer([options][, requestListener]) returns an http.Server. The lifecycle is [source]
- event-driven, and the events you actually wire up are: [source]
- 'request' (req, res) - the normal path; req is an IncomingMessage (a readable [source]
- stream: req.method, req.url, req.headers, req.on('data'|'end')), res is a [source]
- ServerResponse (a writable stream: res.writeHead(status, headers), res.setHeader, [source]
- res.getHeader, res.flushHeaders(), res.write, res.end). [source]
- 'connection' (socket) - a new TCP socket (pre-parse); 'clientError' (err, socket) [source]
- — malformed request or header overflow. The default clientError handler replies `400 Bad [source]
- Request, or 431 on HPE_HEADER_OVERFLOW; override it but always check socket.writable` [source]
- and ignore ECONNRESET. [source]
- 'upgrade' (req, socket, head) - protocol upgrade (WebSocket handshake lives here). [source]
- Client side: http.request(options|url[, callback]) returns a writable [source]
- ClientRequest; http.get is the same but auto-end()s and is GET-only. Key options: [source]
- hostname/host, port (default 80), method (default GET), path, headers, agent, [source]
- timeout. Header size is capped by --max-http-header-size (default 16 KiB), readable as [source]
- http.maxHeaderSize. [source]
- The http.Agent manages the socket pool for outbound requests (the default is [source]
- http.globalAgent, which historically has keepAlive: false). Construct your own to reuse [source]
- connections. Options and defaults: [source]
2. `node:http` — server & socket timeouts (the anti-slowloris knobs)
- These four properties are the most operationally important thing in the module. Misconfigured, [source]
- they cause hung requests, leaked sockets, and the infamous 502 behind a load balancer: [source]
- server.headersTimeout (default 60000 ms) - max time to receive the complete request [source]
- headers. Defeats slowloris header-dribbling. [source]
- server.requestTimeout (default 300000 ms / 5 min) - max time from socket connect to the [source]
- full request being received. Defeats slow-body attacks. [source]
- server.keepAliveTimeout (default 5000 ms) - how long an idle keep-alive socket stays [source]
- open between requests. Must be larger than the upstream load-balancer / proxy idle timeout, [source]
- or the LB reuses a socket Node just closed → ECONNRESET surfaces as a 502. (AWS ALB idle is 60s; [source]
- set Node's keepAliveTimeout above that.) [source]
- server.maxRequestsPerSocket (default unlimited) - close a keep-alive socket after N requests. [source]
- server.timeout (legacy socket inactivity timeout) and server.setTimeout() still exist but the [source]
- three above are the modern, attack-aware controls. [source]
3. `node:http2` — secure/insecure servers, sessions, streams, ALPN, compatibility API
- http2.createServer() = cleartext h2c (rarely used by browsers); http2.createSecureServer({ key, cert }) = h2 over TLS and the one browsers speak - it advertises ALPN 'h2' automatically. allowHTTP1: true lets a secure server fall back to HTTP/1.1 for non-h2 clients. [source]
- Streams, not connections. A single TCP connection (Http2Session) multiplexes many [source]
- Http2Streams. Server side: server.on('stream', (stream, headers) => { stream.respond({ ':status': 200 }); stream.end(body); }). Client: http2.connect(authority) returns a ClientHttp2Session; session.request(headers) returns a ClientHttp2Stream that emits 'response'. [source]
- Pseudo-headers (:method, :path, :scheme, :authority, :status) replace the request line. [source]
- session.settings() tunes initialWindowSize (default 65535), maxConcurrentStreams, [source]
- enablePush. Sessions emit 'goaway' (graceful shutdown) and 'frameError'. [source]
- Server push (stream.pushStream) is deprecated - RFC 9113 removed it and Chrome/modern [source]
- browsers no longer support it. Prefer 103 Early Hints (res.writeEarlyHints) for preloading. [source]
- Stream priority signaling is likewise deprecated. [source]
- Compatibility API: Http2ServerRequest/Http2ServerResponse mimic http's [source]
- IncomingMessage/ServerResponse so Express-style (req, res) handlers run on h2 with minimal [source]
- change. respondWithFile/respondWithFD stream a file/FD directly. [source]
4. `node:https` + `node:tls` — secure context, SNI, ALPN, session resumption
- node:https is HTTP semantics carried over node:tls: https.createServer(options, listener) [source]
- and https.request take the same shape as their http counterparts plus TLS options. There is a [source]
- dedicated https.Agent, which additionally keeps a client-side TLS session cache (keyed by [source]
- host) so reconnections can resume the TLS session and skip a round trip - a meaningful win for a [source]
- keep-alive-light, many-origins client (maxCachedSessions bounds it). [source]
- The real depth is node:tls: [source]
- tls.createSecureContext({ key, cert, ca, pfx, passphrase, minVersion, maxVersion, ciphers }) — [source]
- the reusable cert/key bundle. ca overrides the default trust store; minVersion: 'TLSv1.2' is the [source]
- SNI (one server, many certs): server option SNICallback(servername, cb) or [source]
- **server.addContext('*.example.com', ctx)** picks the cert by requested hostname. Client: [source]
- servername sets the SNI hostname. [source]
- ALPN: ALPNProtocols: ['h2', 'http/1.1'] on server and client negotiates the protocol; read [source]
- the result from socket.alpnProtocol (false if none). This is exactly how h2-vs-h1.1 is chosen. [source]
- Session resumption (skip the full handshake on reconnect), two mechanisms: [source]
- session IDs (server caches state; 'newSession'/'resumeSession' events) and TLS tickets [source]
- (server encrypts state into a ticket the client returns; no server cache, and ticketKeys / [source]
- getTicketKeys/setTicketKeys let a fleet share keys behind a load balancer). Client saves the [source]
- 'session' event buffer and passes it back as session: to tls.connect. sessionTimeout bounds it. [source]
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]) [source]
- emits 'connection' (socket); net.connect/net.createConnection open a client [source]
- net.Socket (a Duplex stream emitting 'data', 'end', 'close', 'error', 'timeout', [source]
- 'ready'). The socket controls you reach for: [source]
- socket.setNoDelay(true) disables Nagle's algorithm (send small writes immediately instead [source]
- of coalescing) - important for low-latency request/response and chatty protocols. [source]
- socket.setKeepAlive(true, delay) enables TCP-level keep-alive probes (detect dead peers). [source]
- socket.setTimeout(ms) fires 'timeout' on inactivity (it does not auto-close - you must [source]
- socket.destroy() in the handler). [source]
- allowHalfOpen (default false): when the remote sends FIN (readable 'end'), Node by default [source]
- also ends the writable side; set true to keep writing after the peer is done reading. [source]
- pauseOnConnect lets you hand a socket to another process before data flows. net.BlockList [source]
- (addAddress/addRange/addSubnet) does IP allow/deny lists. *(Backpressure mechanics of the [source]
- socket stream live in nodejs-concurrency-internals.)* [source]
6. `node:dgram` — UDP sockets (brief)
- Connectionless UDP. dgram.createSocket('udp4'|'udp6') → a socket you bind([port]) and read [source]
- via the 'message' (msg, rinfo) event; socket.send(msg, port, address) to transmit (no [source]
- connection, no delivery guarantee). socket.connect(port, address) pins a default remote so you [source]
- can send(msg) without re-specifying it. Multicast: addMembership/dropMembership, [source]
- setMulticastTTL, setMulticastLoopback; broadcast: setBroadcast(true). Used for DNS, mDNS/SSDP [source]
- discovery, metrics (StatsD), and as the substrate under QUIC/HTTP-3. [source]
7. The global `fetch` **is** undici — Dispatcher, Client, Pool, Agent
- Node's global fetch/Request/Response/Headers (stable since v21) is implemented by [source]
- undici, Node's from-scratch HTTP/1.1 client. Understanding undici is understanding fetch's [source]
- Dispatcher is the base abstraction; everything is a dispatcher with a .dispatch() (and the [source]
- higher-level request/stream/pipeline/connect/upgrade methods). The concrete types: [source]
- Client - a single keep-alive connection to one origin. [source]
- Pool - a pool of Clients to one origin (option connections); this is what gives you [source]
- parallelism to a single host. [source]
- BalancedPool - spreads load across multiple upstream origins. [source]
- Agent - the default dispatcher: opens a Pool per origin on demand (this backs fetch). [source]
- undici.request(url, opts) returns { statusCode, headers, body } where body is a stream with [source]
- convenience readers (body.json(), body.text()); it's lower-overhead than fetch when you don't [source]
- need the WHATWG semantics. undici.stream/pipeline are for zero-copy piping. [source]
- setGlobalDispatcher(dispatcher) / getGlobalDispatcher() swap the dispatcher that **global [source]
- fetch uses** - the supported way to set client-wide pool size, timeouts, TLS (connect options), [source]
- or a proxy for all fetch calls in a process. [source]
- Interceptors compose behaviour onto a dispatcher: dispatcher.compose(interceptor, ...) with [source]
- built-ins for redirect, retry, dns, and cache (the modern replacement for the older [source]
- maxRedirections option style). RetryAgent wraps a dispatcher with a RetryHandler (backoff, [source]
- idempotent-method retries). ProxyAgent / EnvHttpProxyAgent route through an HTTP(S) proxy [source]
- (the latter reads HTTP_PROXY/HTTPS_PROXY/NO_PROXY). MockAgent + setGlobalDispatcher [source]
- intercepts requests in tests without a real network. [source]
8. undici keep-alive & timeout options (the client-side mirror of §2)
- Client/Pool constructor options and their current defaults (verify against your undici [source]
- version - these changed historically): [source]
- pipelining - default off (effectively 1 in-flight per connection); HTTP/1.1 pipelining is [source]
- off because of head-of-line blocking. Set higher only against servers you control. [source]
- keepAliveTimeout - default 4 s; keepAliveMaxTimeout - default 10 min (caps how far [source]
- a server keep-alive hint can extend it); keepAliveTimeoutThreshold trims a safety margin. [source]
- headersTimeout - default 30 s (wait for response headers); bodyTimeout - default [source]
- 30 s (max gap between body chunks). A connection-establishment timeout (~10 s) is configured as a [source]
- Connector option (connect: { timeout }), not a top-level Client default. [source]
- connect: { ... } carries TLS options (ca, rejectUnauthorized, servername, ALPN) for HTTPS [source]
- origins; maxRequestsPerClient recycles a connection after N requests. [source]
Methodology / practical patterns
- Always set client keep-alive. A bare http.request with the default globalAgent [source]
- (keepAlive: false) opens and tears down a TCP+TLS connection per request. Use a shared [source]
- new http.Agent({ keepAlive: true, maxSockets: <bounded> }), or for fetch call [source]
- setGlobalDispatcher(new Agent({ connections: N })) once at startup. [source]
- Order the timeout sandwich correctly: Node server.keepAliveTimeout > upstream LB idle [source]
- timeout, and give headersTimeout/requestTimeout finite values so a stuck client can't pin a [source]
- socket forever. Mirror it on the client with undici headersTimeout/bodyTimeout. [source]
- Bound maxSockets/connections. Infinity (the default) means a downstream slowdown lets [source]
- pending requests open unbounded sockets → fd exhaustion. Size the pool to the downstream's capacity. [source]
- Pick the protocol deliberately: HTTP/2 (createSecureServer + ALPN 'h2') for many concurrent [source]
- streams to one origin; HTTP/1.1 + a Pool of connections when the server isn't h2. Don't enable [source]
- HTTP/1.1 pipelining on the open internet. [source]
- Reuse a SecureContext across connections instead of re-reading PEM per request; enable session [source]
- resumption (tickets + shared ticketKeys behind an LB) to cut handshake round-trips. [source]
- Test with MockAgent, not a live network: const mock = new MockAgent(); setGlobalDispatcher(mock); mock.get(origin).intercept({ path }).reply(200, body). [source]
Anti-patterns
- No timeouts anywhere. A fetch/http.request with no bodyTimeout/headersTimeout to a slow [source]
- peer hangs forever and holds a socket; a server with the defaults removed is a slowloris target. [source]
- keepAliveTimeout below the LB idle timeout → the LB reuses a socket Node already closed → [source]
- ECONNRESET → intermittent 502s that look random. The #1 Node-behind-ALB bug. [source]
- maxSockets: Infinity / unbounded connections → socket & file-descriptor exhaustion under load [source]
- (EMFILE), often mistaken for a memory leak. [source]
- A fresh Agent/Pool/Client per request → you've thrown away pooling entirely; create it once [source]
- Enabling HTTP/1.1 pipelining to arbitrary servers → head-of-line blocking and corruption with [source]
- non-compliant intermediaries; that's why undici ships it off. [source]
- Relying on HTTP/2 server push → removed from browsers and deprecated in RFC 9113; use `103 Early [source]
- Disabling rejectUnauthorized to "fix" a TLS error → silently disables cert validation (MITM). [source]
- Fix the trust chain via ca: instead. [source]
Troubleshooting
- Intermittent 502 / ECONNRESET behind a proxy → raise server.keepAliveTimeout above the [source]
- upstream idle timeout; confirm with curl -v keep-alive reuse. [source]
- fetch is slow / opens too many connections → you're on the default per-origin pool; install a [source]
- tuned Agent via setGlobalDispatcher and check keepAlive is in effect. [source]
- socket hang up / UND_ERR_HEADERS_TIMEOUT / UND_ERR_BODY_TIMEOUT → the server didn't respond [source]
- within undici's 30 s header/body timeout; raise the relevant option or fix the upstream. [source]
- EMFILE: too many open files → unbounded maxSockets/connections (or leaked sockets that never [source]
- end); bound the pool and ulimit -n. [source]
- HPE_HEADER_OVERFLOW / 431 → headers exceed --max-http-header-size (16 KiB); raise the flag or [source]
- shrink cookies/headers. [source]
- HTTP/2 client gets HTTP/1.1 → ALPN didn't negotiate 'h2'; check ALPNProtocols on both ends and [source]
- read socket.alpnProtocol to confirm. [source]
- TLS handshake slow under load → no session resumption; wire up tickets/ticketKeys and reuse a [source]
- single SecureContext. (Event-loop lag while throughput is fine is a different problem - profile the [source]
- loop; see nodejs-concurrency-internals.) [source]
References
- Node.js - node:http (Server, IncomingMessage/ServerResponse, http.request/get, http.Agent, headersTimeout/requestTimeout/keepAliveTimeout/maxRequestsPerSocket, clientError, maxHeaderSize): https://nodejs.org/api/http.html [source]
- Node.js - CLI options (--max-http-header-size): https://nodejs.org/api/cli.html [source]
- Node.js - node:http2 (createServer/createSecureServer, http2.connect, Http2Session/Http2Stream, pushStream deprecation, ALPN, settings, compatibility API): https://nodejs.org/api/http2.html [source]
- Node.js - node:tls (createSecureContext, SNICallback/addContext, ALPNProtocols/alpnProtocol, session resumption - IDs vs tickets, ticketKeys, sessionTimeout): https://nodejs.org/api/tls.html [source]
- Node.js - node:https (createServer/request, https.Agent + TLS session cache): https://nodejs.org/api/https.html [source]
- Node.js - node:net (createServer, net.Socket, allowHalfOpen, setNoDelay/Nagle, setKeepAlive, setTimeout, BlockList): https://nodejs.org/api/net.html [source]
- Node.js - node:dgram (UDP createSocket, send/bind, 'message', multicast addMembership, connected UDP): https://nodejs.org/api/dgram.html [source]
- Node.js - global fetch / WHATWG fetch backed by undici: https://nodejs.org/api/globals.html#fetch [source]
- undici - Dispatcher/Client/Pool/BalancedPool/Agent, request/stream/pipeline, setGlobalDispatcher, interceptors, RetryAgent/ProxyAgent/EnvHttpProxyAgent/MockAgent: https://undici.nodejs.org/ [source]
- undici - Client API options & defaults (pipelining, keepAliveTimeout 4s, keepAliveMaxTimeout 10min, headersTimeout 30s, bodyTimeout 30s): https://github.com/nodejs/undici/blob/main/docs/docs/api/Client.md [source]
Children
- node:http server lifecycle, IncomingMessage/ServerResponse, http.request, http.Agent (frontier)
- node:http server & socket timeouts (headersTimeout/requestTimeout/keepAliveTimeout/maxRequestsPerSocket) (frontier)
- node:http2 (secure/insecure servers, sessions/streams, ALPN, server push, compatibility API) (frontier)
- node:https + node:tls (createSecureContext, SNI, ALPN, session resumption) (frontier)
- node:net TCP connection model (allowHalfOpen, Nagle/setNoDelay, setKeepAlive, BlockList) (frontier)
- node:dgram UDP sockets and multicast (frontier)
- The global fetch as undici (Dispatcher/Client/Pool/Agent, interceptors, RetryAgent/ProxyAgent/MockAgent, setGlobalDispatcher) (frontier)
- undici keep-alive & timeout options (pipelining, keepAliveTimeout, keepAliveMaxTimeout, headersTimeout, bodyTimeout) (frontier)
Frontier under this node: The global fetch as undici (Dispatcher/Client/Pool/Agent, interceptors, RetryAgent/ProxyAgent/MockAgent, setGlobalDispatcher), node:dgram UDP sockets and multicast, node:http server & socket timeouts (headersTimeout/requestTimeout/keepAliveTimeout/maxRequestsPerSocket), node:http server lifecycle, IncomingMessage/ServerResponse, http.request, http.Agent, node:http2 (secure/insecure servers, sessions/streams, ALPN, server push, compatibility API), node:https + node:tls (createSecureContext, SNI, ALPN, session resumption), node:net TCP connection model (allowHalfOpen, Nagle/setNoDelay, setKeepAlive, BlockList), undici keep-alive & timeout options (pipelining, keepAliveTimeout, keepAliveMaxTimeout, headersTimeout, bodyTimeout)