Native fetch socket pool mechanics, Web Crypto vs libuv thread pool, and profiling ReadableStream memory leaks under RPC load.
Module 19 — The Modern Web Standard Shift: Undici, fetch, and Web Crypto
What this module covers: Node.js 18 shipped native
fetch— built on Undici, a from-scratch HTTP/1.1 and HTTP/2 client that replaced the legacyhttp.requestinternals. The connection pool model changed. Garbage collection behavior changed. Performance characteristics changed. Engineers who understandhttp.requestsemantics may be surprised by subtle differences in howfetchhandles keep-alive, connection reuse, and stream consumption. This module covers Undici's architecture, how to avoidReadableStreammemory leaks when consuming large RPC responses, and the Web Crypto API's standard, Promise-native surface for cryptographic operations — which still runs on the same libuv thread pool as legacycrypto, and where that matters for hot-path performance.
Undici: The New HTTP Client Foundation
undici is the HTTP client library underlying Node.js's native fetch. It was written from scratch (unlike the legacy http module which dates to 2009) with modern HTTP semantics, connection pooling, and proper backpressure.
Connection Pool Architecture
Unlike http.request which uses a per-hostname keep-alive agent, Undici uses a Pool or Client with configurable connection counts. Undici's pool is a fleet of taxis outside the RPC node's building: fetch grabs whatever cab is idling, while Pool.request lets you pre-book exactly how many cabs wait there and how long each engine idles between fares.
Client and Pool solve different problems. A Client manages exactly one connection to a single origin — use it when you're deliberately talking to one specific backend instance and want to reason about a single socket's lifecycle (e.g. a sticky connection to one RPC node replica). A Pool manages multiple connections to a single origin and load-balances requests across them — this is what you want for the general case of calling one RPC service at high concurrency, which is why the rest of this module uses Pool.
A caveat on pipelining: 10: HTTP/1.1 pipelining sends multiple requests on a connection before earlier responses arrive, but the responses must still come back in order. Pipelining assumes every queued request will succeed and that the connection never needs a mid-stream fallback. If one pipelined request fails or the connection drops partway through, Undici cannot safely retry or reorder the requests still queued behind it on that connection — they fail too, even though nothing was wrong with them individually. This is why pipelining is not a free 10x: it trades resilience under partial failure for lower per-request overhead, and it's a poor fit for RPC endpoints with any tail latency variance or occasional 5xx responses. Most production Undici pools leave pipelining at the default of 1 (no pipelining) and get their concurrency from connections instead — reach for pipelining only when the upstream is known to be reliable and strictly FIFO-safe.
ReadableStream Memory Leaks: The Critical Mistake
Every fetch response (and Undici response) has a body that is a ReadableStream. If you don't consume the body, the connection is not released back to the pool:
Production story: during a UPI transaction spike, a service's RPC calls started timing out intermittently, and the on-call engineer's first theory was that the upstream RPC provider was down. It wasn't. A health-check probe running every few seconds called fetch(rpcHealthUrl), and on any non-200 response threw immediately without draining the body — exactly the LEAK pattern below. Each failed health check silently held one connection open. The pool was configured for 20 connections; within a few minutes of the RPC node returning intermittent 503s under load, every connection in the pool was pinned to an unconsumed body, and legitimate transaction-verification calls queued behind health checks that would never release their socket. The fix was the same body-draining discipline shown below — but the diagnosis cost 40 minutes because the symptom (RPC timeouts) pointed everyone at the RPC provider instead of the caller's own connection pool.
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion