mrkeyoor.com_
Wed 05 Aug 05:06 UTC
npmWeb Backendupdated 05 Aug 2026

undici

Undici is the HTTP/1.1 client the Node.js project wrote from scratch, and it is what powers the fetch() built into Node since v18. Installing it as a module gets you the newest version of that engine plus the low-level APIs the built-in fetch hides: request, stream, pipeline, and dispatch, along with connection pools, pipelining, interceptors for retry and caching, ProxyAgent and Socks5Agent for proxying, and MockAgent for network-free tests. It has zero runtime dependencies and, in the project's own benchmarks, undici.request runs several times faster than node-fetch and clearly ahead of axios and got.

Verdict

This is the engine behind Node's own fetch, maintained inside the nodejs org, and the fastest mainstream HTTP client for Node by its own published benchmarks. Install it when you need the pooling, mocking, proxy, or interceptor controls the built-in fetch does not expose; if built-in fetch covers you, adding the module buys you little.

API stability4/5The core request/fetch/Agent APIs have been steady for years, but majors land fast (v8 shipped April 2026 and raised the floor to Node 22.19), and the interceptor and cache APIs are newer surfaces that still move.
Docs4/5undici.nodejs.org has per-API reference pages and the README itself covers benchmarks, fetch-vs-module tradeoffs, caching, and migration; low-level dispatcher composition still takes effort to piece together.
Maintenance5/5Lives in the nodejs GitHub org, ships inside Node core as the fetch implementation, and is under near-daily development (last push 2026-08-04, 7.6k stars, active release cadence).
Ecosystem4/5154M weekly downloads and it underpins Node's global fetch, so indirect reach is enormous; direct third-party extension ecosystem is thin compared to axios middleware culture since most integration happens inside frameworks.

Use it if

  • You need connection pooling control, HTTP/1.1 pipelining, or interceptors (retry, cache, DNS) that Node's built-in fetch does not expose
  • Service-to-service HTTP performance matters; the project's benchmarks put undici.request at roughly 3.5x node-fetch on the same workload
  • You need MockAgent to intercept requests in tests without touching the network, or ProxyAgent/Socks5Agent for outbound proxying
  • You want a newer fetch implementation than the one bundled with your Node runtime (check process.versions.undici to see what you have)
Skip it if

Setup reality

npm i undici is clean: zero runtime dependencies, no build step, no peer deps. The catches are elsewhere. Version 8 requires Node >= 22.19.0, a real cliff if you are on Node 20 or 22 pre-.19. The request API hands you a body stream that you must consume (or call .dump()) even on error paths, otherwise the connection is not released back to the pool and things stall under load. Composing dispatchers (Agent, compose, interceptors) is its own mental model and the docs assume you will read them. Error shapes also differ between undici.fetch, which wraps failures in TypeError per the spec, and undici.request, which throws undici's own error classes.

Patterns

GET a JSON response with request()get-json

import { request } from 'undici'

const { statusCode, headers, body } = await request('https://api.example.com/data')
if (statusCode !== 200) {
  await body.dump()
  throw new Error(`unexpected status ${statusCode}`)
}
const data = await body.json()

Always consume or dump() the body, even on error paths, or the connection never returns to the pool.

POST a JSON payloadpost-json

import { request } from 'undici'

const { statusCode, body } = await request('https://api.example.com/items', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'widget', qty: 2 })
})
const created = await body.json()

request() does not throw on 4xx/5xx like fetch does not; check statusCode yourself.

Use undici's fetch instead of the built-infetch-drop-in

import { fetch } from 'undici'

const res = await fetch('https://api.example.com/data')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()

This gets you the latest fetch spec behavior instead of whatever undici version your Node binary bundled; check process.versions.undici to compare.

Tune the connection pool globallyconnection-pool

import { Agent, setGlobalDispatcher } from 'undici'

setGlobalDispatcher(new Agent({
  connections: 128,
  keepAliveTimeout: 10_000,
  pipelining: 1
}))
// every undici request/fetch in the process now uses this pool

setGlobalDispatcher affects undici module calls; Node's built-in global fetch keeps its own bundled dispatcher.

Retry failed requests with RetryAgenthttp-retry

import { Agent, RetryAgent, request } from 'undici'

const agent = new RetryAgent(new Agent(), {
  maxRetries: 3,
  minTimeout: 500,
  statusCodes: [429, 500, 502, 503, 504]
})
const { statusCode, body } = await request('https://api.example.com/flaky', {
  dispatcher: agent
})

By default only idempotent situations are safe to retry; think twice before adding POST to the retry set.

Send requests through an HTTP proxyproxy

import { ProxyAgent, request } from 'undici'

const proxy = new ProxyAgent('http://proxy.internal:8080')
const { body } = await request('https://api.example.com/data', {
  dispatcher: proxy
})
const data = await body.json()

ProxyAgent is per-request via dispatcher or global via setGlobalDispatcher; there is also a Socks5Agent for SOCKS proxies.

Mock HTTP calls in tests with MockAgentmock-testing

import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
mockAgent.disableNetConnect()
setGlobalDispatcher(mockAgent)

mockAgent
  .get('https://api.example.com')
  .intercept({ path: '/users/1', method: 'GET' })
  .reply(200, { id: 1, name: 'ada' })

const { body } = await request('https://api.example.com/users/1')
console.log(await body.json()) // { id: 1, name: 'ada' }

disableNetConnect() makes unmocked calls throw instead of silently hitting the real network; use it.

Set headers and body timeoutstimeouts

import { Agent, request } from 'undici'

const agent = new Agent({
  headersTimeout: 10_000, // ms to wait for response headers
  bodyTimeout: 30_000     // ms of body inactivity allowed
})
const { body } = await request('https://api.example.com/slow', {
  dispatcher: agent
})

bodyTimeout is an inactivity timeout between chunks, not a total-duration cap; combine with AbortSignal.timeout for a hard deadline.

Cancel a request with an AbortSignalabort-request

import { request } from 'undici'

try {
  const { body } = await request('https://api.example.com/slow', {
    signal: AbortSignal.timeout(5000)
  })
  const data = await body.json()
} catch (err) {
  if (err.name === 'TimeoutError' || err.name === 'AbortError') {
    // deadline hit
  } else throw err
}

AbortSignal.timeout gives a total-duration deadline, which the per-phase Agent timeouts alone do not.

Stream a large response bodystream-response

import { request } from 'undici'
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'

const { body } = await request('https://example.com/big-file.tar.gz')
await pipeline(body, createWriteStream('/tmp/big-file.tar.gz'))

body is an async-iterable Node stream; piping it avoids buffering the whole payload in memory.

Cache responses with the cache interceptorhttp-cache

import { fetch, Agent, interceptors, setGlobalDispatcher } from 'undici'
import { cacheStores } from 'undici'

const client = new Agent().compose(interceptors.cache({
  store: new cacheStores.MemoryCacheStore({ maxSize: 100 * 1024 * 1024 }),
  methods: ['GET', 'HEAD']
}))
setGlobalDispatcher(client)

const res = await fetch('https://api.example.com/data') // cached per Cache-Control

Caching honors the server's Cache-Control/Expires headers; if the origin sends no cache headers, nothing gets cached.

Upload multipart form dataform-data-upload

import { fetch, FormData } from 'undici'
import { openAsBlob } from 'node:fs'

const form = new FormData()
form.set('file', await openAsBlob('./report.pdf'), 'report.pdf')
form.set('note', 'monthly report')

await fetch('https://api.example.com/upload', { method: 'POST', body: form })

Keep fetch and FormData from the same implementation (both undici imports or both globals); mixing them is a documented failure mode.

Alternatives

PackageRegistryPick it when
axiosnpmYou want the most widely known API with interceptors and browser support in one package
gotnpmYou want retries, hooks, and pagination built in for Node scripts and backends
kynpmYou want a tiny fetch wrapper with retries that works in browsers and Node alike