Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nodejs/undici/llms.txt

Use this file to discover all available pages before exploring further.

Node.js has shipped a built-in fetch() implementation powered by undici since Node.js v18. Both options share the same underlying HTTP engine, but they expose different capabilities. This guide explains the differences, shows the performance data, and helps you decide which to use for your project.

Background: they share the same engine

The fetch(), Request, Response, Headers, and FormData globals in Node.js v18+ come from a version of undici that is bundled directly into the Node.js binary. Installing undici from npm gives you the same library — but potentially a newer release — plus all of its additional APIs that are not exposed as globals. You can check which version of undici is currently bundled with your Node.js runtime:
Checking the bundled undici version
console.log(process.versions.undici) // e.g., "7.5.0"

Comparison

FeatureBuilt-in fetchundici module
Standard Fetch APIYesYes
request() / stream() / pipeline()NoYes
ProxyAgent / Socks5ProxyAgentNoYes
MockAgent / MockClient / MockPoolNoYes
EnvHttpProxyAgentNoYes (--use-env-proxy in Node.js v22.21+)
Custom interceptorsNoYes
Connection pool configurationNoYes
HTTP/1.1 pipeliningNoYes
WebSocket / EventSourceYes (Node.js v22+)Yes
Always uses latest undiciNoYes
Zero additional dependenciesYesNo
Runs in browsers / Deno / BunYesNo

Performance comparison

The benchmark below was run with 50 TCP connections and a pipelining depth of 10 on Node.js 22.11.0. All three undici APIs outperform the built-in http module with keep-alive, and undici.request() reaches over 18,000 req/sec — more than 3x the throughput of axios or node-fetch:
ClientRequests/secvs. slowest
axios5,708baseline
node-fetch5,945+4.15%
undici fetch5,903+3.43%
http (keepalive)9,193+61.05%
undici pipeline13,364+134.13%
undici stream18,245+219.63%
undici request18,340+221.29%
undici dispatch22,234+289.51%
undici.fetch() and the built-in global fetch perform similarly. The speed advantage of the undici module comes from request(), stream(), and pipeline(), which bypass the Web Streams layer.

When to use the built-in globals

You do not need to install undici when all of the following are true:
  • You only need the standard Fetch API (fetch, Request, Response, Headers, FormData).
  • You are running Node.js v18 or later.
  • You do not depend on features or bug fixes from a newer undici release than the one bundled with your Node.js version.
  • You want zero additional runtime dependencies.
  • You are writing isomorphic code that also runs in browsers, Deno, Bun, or Cloudflare Workers.
Using the built-in fetch globally (Node.js v18+)
// No import needed — fetch is a global
const response = await fetch('https://api.example.com/data')
const data = await response.json()

When to install the undici module

Install undici from npm when you need any of the following:
request(), stream(), and pipeline() provide lower-level access and significantly better throughput than fetch(). Use them for high-volume services or data-intensive workloads.
Using request() for performance
import { request } from 'undici'

const { statusCode, headers, body } = await request('https://api.example.com/data')
const data = await body.json()
Client, Pool, BalancedPool, Agent, and their options let you control keep-alive behavior, pipelining depth, and concurrency per origin.
Configuring a connection pool
import { Pool } from 'undici'

const pool = new Pool('https://api.example.com', { connections: 10 })
const { body } = await pool.request({ path: '/users', method: 'GET' })
const users = await body.json()
ProxyAgent and EnvHttpProxyAgent provide full programmatic proxy configuration. While Node.js v22.21.0+ supports --use-env-proxy for built-in fetch, undici’s ProxyAgent gives you per-request dispatcher control.
Routing through a proxy
import { ProxyAgent, fetch } from 'undici'

const proxyAgent = new ProxyAgent('https://my-proxy.example.com:8080')
const response = await fetch('https://api.example.com/data', {
  dispatcher: proxyAgent
})
MockAgent, MockClient, and MockPool let you intercept and assert on HTTP traffic in tests without running a real server or patching globals.
Mocking with MockAgent
import { MockAgent, setGlobalDispatcher, fetch } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const pool = mockAgent.get('https://api.example.com')
pool.intercept({ path: '/users' }).reply(200, [{ id: 1, name: 'Alice' }])

const response = await fetch('https://api.example.com/users')
const users = await response.json()
console.log(users) // [{ id: 1, name: 'Alice' }]
Compose interceptors for caching, retry, DNS, redirect, and deduplication on any dispatcher without modifying request call sites.
Composing interceptors
import { Agent, interceptors, cacheStores } from 'undici'

const agent = new Agent().compose(
  interceptors.cache({
    store: new cacheStores.MemoryCacheStore({ maxCount: 1000 })
  }),
  interceptors.retry({ maxRetries: 3 })
)

Migration: from built-in fetch to undici

If you are currently using the built-in global fetch and want to migrate to undici, here are your options in increasing order of performance:

Drop-in fetch replacement

Import fetch from undici instead of using the global. The API is identical:
Switching to undici fetch
// Before: built-in global
const response = await fetch('https://api.example.com/data')
const data = await response.json()

// After: undici module (identical call signature)
import { fetch } from 'undici'

const response = await fetch('https://api.example.com/data')
const data = await response.json()

Migrate to request() for better performance

request() returns statusCode, headers, and body directly — no Response wrapper — which eliminates the Web Streams overhead:
Migrating from fetch to request()
// Before: built-in fetch
const response = await fetch('https://api.example.com/data')
const data = await response.json()

// After: undici.request() — same result, better throughput
import { request } from 'undici'

const { statusCode, body } = await request('https://api.example.com/data')
const data = await body.json()

Replace the global dispatcher

To use undici’s fetch globally (replacing the built-in), call install():
Replacing global fetch with undici
import { install } from 'undici'

install()

// Now the global fetch, Headers, Response, Request, FormData, WebSocket,
// and EventSource all come from undici
const response = await fetch('https://api.example.com/data')

Keep fetch and FormData from the same implementation

Do not mix FormData from one implementation with fetch from another. The multipart encoding may differ between undici versions, causing subtle bugs.
When uploading FormData, use a consistent pair. Pick one of these patterns:
Using built-in FormData with built-in fetch
const body = new FormData()
body.set('name', 'Alice')
body.set('role', 'admin')

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

Version compatibility table

Node.js versionBundled undicifetch statusNotes
v18.x~5.xExperimental (early), then stableBehind --experimental-fetch in early v18
v20.x~6.xStable
v22.x~6.x / ~7.xStable--use-env-proxy in v22.21.0+
v24.x~7.xStable--use-env-proxy enabled by default
Installing undici from npm does not replace the built-in globals automatically. Use install() if you want your installed version to replace globalThis.fetch and friends. Otherwise, import directly:
Importing undici fetch explicitly
import { fetch } from 'undici' // your installed version, not the built-in global
Run console.log(process.versions.undici) at any time to see the exact undici version bundled with your Node.js runtime. If you need a newer version, install undici from npm.

Build docs developers (and LLMs) love