Module A-20·11 min read

Native fetch socket pool mechanics, Web Crypto vs libuv thread pool, and profiling ReadableStream memory leaks under RPC load.

JJS
Written by Jatin Jain Saraf · Senior Software Engineer

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 legacy http.request internals. The connection pool model changed. Garbage collection behavior changed. Performance characteristics changed. Engineers who understand http.request semantics may be surprised by subtle differences in how fetch handles keep-alive, connection reuse, and stream consumption. This module covers Undici's architecture, how to avoid ReadableStream memory 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 legacy crypto, 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.

javascript

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.

javascript
javascript

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 & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.