nodejs/undici
> An HTTP/1.1 client written from scratch for Node.js — and the engine behind Node's built-in fetch.
GitHub repo · Official website · License: MIT
Overview
Undici is a low-level HTTP/1.1 client for Node.js, started in 2018 by Matteo Collina and now maintained under the nodejs org1. The name is Italian for "eleven" — a pun on HTTP/1.1 (and a Stranger Things reference). Unlike got, axios, or node-fetch, it does not wrap Node's built-in http module; it speaks the wire protocol directly over sockets, which is where most of its throughput advantage comes from.
Its reach is much larger than its star count (~7.6k) suggests. Since Node.js 18 (2022), the global fetch, Headers, Response, Request, FormData, WebSocket, and EventSource are all implemented by a bundled copy of undici2. So virtually every modern Node process already runs undici whether the author installed it or not. The standalone npm package exists to expose the faster low-level API (request, stream, pipeline, dispatch) and features that the WHATWG fetch surface deliberately hides — connection-pool tuning, interceptors, ProxyAgent, MockAgent.
The defining tension is spec-compliance versus performance. undici.fetch() is a faithful, and therefore relatively heavy, implementation of the WHATWG Fetch standard (Web Streams, body cloning, spec-mandated buffering). undici.request() is the same HTTP core with a Node-native, non-spec API that is several times faster. Choosing undici well means knowing which of its two front doors you are walking through.
Getting Started
npm i undici
import { request } from 'undici'
// Low-level API — fastest path, Node-native ergonomics
const { statusCode, headers, body } = await request('https://api.example.com/data')
console.log(statusCode)
const json = await body.json() // body is a consumable stream + mixins
import { Agent, setGlobalDispatcher } from 'undici'
// Tune the connection pool that all requests (and global fetch) will use
setGlobalDispatcher(new Agent({
keepAliveTimeout: 10_000,
connections: 128, // max sockets per origin
pipelining: 1, // requests in flight per socket
}))
Architecture / How It Works
The central abstraction is the Dispatcher. Everything — fetch, request, stream, pipeline — ultimately calls dispatcher.dispatch(). Concrete dispatchers form a hierarchy:
- Client — a single persistent connection to one origin. The lowest unit; holds one socket, an outgoing request queue, and an HTTP/1.1 parser.
- Pool — a set of
Clients to the same origin, load-balanced across sockets. This is what most apps actually want for one host. - BalancedPool — spreads load across multiple upstream origins.
- Agent — the default global dispatcher; lazily creates a
Poolper origin as new hosts are requested.getGlobalDispatcher()/setGlobalDispatcher()swap it.
The HTTP/1.1 parser is a WebAssembly build of llhttp (the same parser Node core uses), which avoids per-byte JS overhead. Sockets are kept alive and, optionally, pipelined (multiple requests sent before responses return) — off by default because many real-world servers and proxies mishandle it.
Interceptors wrap a dispatcher via .compose(), forming a middleware chain: redirect following, retry, response caching, DNS caching, and dump-on-abort all ship as composable interceptors rather than being baked into the core. This keeps the hot path lean and makes behavior opt-in.
undici.fetch() is built on top of this dispatcher core as a separate spec-compliance layer: it adds Web Streams bodies, CORS-mode semantics (mostly inert in Node), and the Request/Response/Headers classes. HTTP/2 exists but is opt-in per-client via the allowH2 option and is less mature than the HTTP/1.1 path3.
Production Notes
fetchvsrequestis a real performance decision. The Fetch spec forces body buffering and Web Streams;undici.request()skips all of it. On the project's own benchmark,undici.request/stream/dispatchrun 2–4× the throughput ofundici.fetch4. If you control both ends and don't need spec semantics, userequest.- A response body is a stream you must consume. Ignoring
bodyleaks the socket back-pressure and can stall the pool. If you don't need the body, callbody.dump()or consume it. This is the single most common undici footgun. - Global-vs-installed version skew.
process.versions.undiciis the copy baked into your Node runtime;npm i undicilayers a possibly-newer copy on top. Mixing them causes subtle bugs — most notoriously passing a globalFormDatatoundici.fetch()(or vice-versa), which fails because the twoFormDataclasses differ. Useinstall()to force all globals to the installed copy, or keep both from one source2. - Timeouts are opt-in and multi-layered.
headersTimeoutandbodyTimeoutdefault to non-infinite but generous values;connect.timeoutis separate. A "hung request" is usually a body that never finished streaming, not a dead socket. Set these explicitly for outbound calls to third parties. - Testing.
MockAgentintercepts at the dispatcher layer, so it works transparently for bothfetchandrequestonce set as the global dispatcher — the standard way to stub HTTP in Node tests without monkey-patching. - Observability. Undici publishes to
diagnostics_channel(request/response/error/connect events), which is how APM tools instrument it. There is no built-in logging. - Proxies need
ProxyAgent. Undici does not readHTTP_PROXY/HTTPS_PROXYenvironment variables automatically; you must construct and install aProxyAgentyourself.
When to Use / When Not
Use when:
- You want the fastest HTTP client in Node and can use the
request/stream/dispatchAPI. - You need connection-pool control, pipelining, per-origin tuning, or custom interceptors.
- You need
ProxyAgent,MockAgent, or a standards-compliantWebSocket/EventSourcein Node. - You want to pin a newer
fetch/undici than your Node runtime bundles.
Avoid when:
- You just need occasional requests — the built-in global
fetchis already undici and needs no dependency. - You want browser portability — write to the Web
fetchAPI, not undici's Node-native methods. - You need a batteries-included client with automatic retries, hooks, and pagination out of the box —
gotandaxiosare higher-level. - You need mature first-class HTTP/2 or HTTP/3; undici is HTTP/1.1-first.
Alternatives
- sindresorhus/got — higher-level Node client with retries, hooks, pagination; built on Node http, slower but more ergonomic for app code.
- axios/axios — isomorphic (browser + Node) client with interceptors and a huge install base; use when you want one API across environments.
- node-fetch/node-fetch — the pre-Node-18
fetchpolyfill; now largely obsoleted by built-infetch(which is undici). - nodejs/node built-in
fetch/http— use when you want zero dependencies and standard-only APIs. - szmarczak/http2-wrapper — reach for when HTTP/2 is the primary requirement rather than an afterthought.
History
| Version | Date | Notes |
|---|---|---|
| repo start | 2018-05 | First commit; from-scratch HTTP/1.1 client experiment1. |
| bundled in Node 18 | 2022-04 | Global fetch ships (experimental) powered by undici2. |
stable fetch in Node 21 | 2023-10 | Global fetch unflagged as stable in Node core. |
| 6.0 | 2024 | Major: dropped older Node versions, API cleanup, tree-shakeable build. |
| 7.0 | 2024–2025 | Major: interceptor/dispatcher API refinements, further removals. |
References
- ^ undici repository and history. https://github.com/nodejs/undici
- ^ undici README — "Undici vs. Fetch" and
install()globals. https://github.com/nodejs/undici/blob/main/README.md - ^ undici docs — Dispatcher / Client (
allowH2, connection options). https://undici.nodejs.org/ - ^ undici README benchmarks (Node 24, 50 connections, pipelining 10). https://github.com/nodejs/undici/blob/main/README.md#benchmarks
Tags
javascript, nodejs, http-client, http, fetch, networking, connection-pool, web-standards, performance, library