Node.js HTTP Clients in 2026: undici, fetch, axios & got
Almost every Node.js service is, at its core, a glue layer between other services. You call a payments API, a search cluster, an internal microservice, a webhook receiver — and the quality of those outbound HTTP calls quietly decides your tail latency, your error rate, and your cloud bill. In 2026 the choice of HTTP client is no longer a footnote: it is a real performance and reliability decision.
For years the default answer was "just npm install axios." That is no longer obviously correct. Node.js now ships a fast, standards-based fetch built on undici, and undici itself exposes a lower-level API that consistently tops throughput benchmarks. Meanwhile axios and got remain hugely popular for their ergonomics. This guide compares undici, native fetch, axios, and got head to head — throughput, connection pooling, retries, and developer experience — so you can pick the right tool for each layer of your stack.
Why Your HTTP Client Choice Matters in 2026
Outbound HTTP is where Node.js applications spend a surprising amount of their wall-clock time. When a request handler awaits three downstream calls, the client's connection reuse, DNS caching, and header parsing all sit directly on the critical path of your p99 latency. A client that opens a fresh TCP and TLS connection for every call can add tens of milliseconds per request — multiplied across millions of requests, that is the difference between two replicas and twenty.
There is also a maintenance dimension. Native fetch ships with the runtime, so it has zero dependency surface and no supply-chain risk. Third-party clients add packages to audit and upgrade. If you are building a long-lived Node.js backend, picking a client with a stable, well-supported API saves real engineering time over the project's life.
The three things that actually move the needle
Throughput (how many requests per second a single process can sustain), resilience (retries, timeouts, and circuit breaking), and ergonomics (how little code it takes to do the common thing correctly) are the three axes that matter. No single client dominates all three, which is exactly why this comparison is worth doing carefully.

Meet the Contenders: undici, fetch, axios & got
undici — the speed-first foundation
undici is the HTTP/1.1 client the Node.js core team wrote from scratch for performance. It powers the global fetch implementation, but you can also use its lower-level Client, Pool, and Agent APIs directly. Those APIs expose connection pooling, pipelining, and a built-in mock layer for tests. If you need maximum requests-per-second from a worker or a high-fan-out gateway, undici is the benchmark leader.
native fetch — zero dependencies, web-standard
Since the fetch API is stable in modern Node.js, you can write the same code you would in the browser: await fetch(url) returns a Response with .json(), .text(), and a streaming body. It is built on undici under the hood, so throughput is close to undici's, and it costs you nothing in dependencies. The trade-off is a thinner feature set — no built-in retries, no interceptors — so you build resilience yourself.
axios & got — ergonomics and ecosystem
axios remains the most downloaded HTTP client on npm thanks to its friendly API, request and response interceptors, automatic JSON handling, and enormous ecosystem of plugins. got, from the Sindre Sorhus ecosystem, offers first-class retries, hooks, and a clean promise API that many teams prefer for scripts and integrations. Both are slower than undici under load, but for application code where you make a handful of calls per request, that gap rarely matters.
Connection Pooling: undici's Secret Weapon
The single biggest performance lever in any HTTP client is connection reuse. Establishing a TCP connection and completing a TLS handshake is expensive; doing it once and reusing the socket for thousands of requests is what separates a fast client from a slow one. undici's Pool keeps a configurable set of keep-alive connections open to an origin and dispatches requests across them, so your hot path almost never pays the handshake cost.
import { Pool } from 'undici';
// One pool per origin, reused for the lifetime of the process.
const pool = new Pool('https://api.example.com', {
connections: 64, // max concurrent sockets to this origin
pipelining: 1, // keep at 1 unless the server supports pipelining
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 600_000,
});
export async function getUser(id) {
const { statusCode, body } = await pool.request({
path: `/users/${id}`,
method: 'GET',
headers: { accept: 'application/json' },
});
if (statusCode >= 400) {
throw new Error(`Upstream returned ${statusCode}`);
}
return body.json();
}
// Drain sockets gracefully on shutdown.
process.on('SIGTERM', () => pool.close());Native fetch uses a shared global pool automatically, which is why it is nearly as fast as raw undici for most workloads. axios and got reuse connections only if you pass a Node.js http.Agent with keepAlive enabled — a step many teams forget, which is the most common reason a benchmark shows them as "slow" when the real problem is fresh connections on every call.
Tuning pool size for your workload
The connections option is the dial that matters most. Set it too low and requests queue behind a handful of sockets, inflating latency under load; set it too high and you risk exhausting file descriptors or overwhelming the upstream. A practical starting point is to size the pool to your expected concurrency to that origin — often somewhere between 16 and 128 — then watch socket and queue metrics under realistic traffic and adjust. Because undici exposes pool statistics, you can wire these into your dashboards and treat pool saturation as a first-class signal rather than a mystery.

Hire Pre-Vetted Node.js Developers
Skip the months-long search. Our exclusive talent network has senior Node.js experts ready to join your team in 48 hours.
Performance Benchmarks Compared
In controlled benchmarks on Node.js 24 against a localhost JSON echo server with 50 concurrent connections, undici and native fetch consistently lead, with undici a few percent ahead because it skips the small amount of spec overhead fetch carries. axios and got land lower, primarily because their default configuration does less aggressive connection reuse and does more work per request building their rich request objects.
The radar view below scores each client on five dimensions. The pattern is clear: undici and fetch win on raw speed and footprint, while axios and got win on ergonomics and ecosystem. The right answer depends on which axis your workload actually cares about.
Memory footprint is the quieter half of the performance story. undici and native fetch allocate less per request and hold fewer abstractions in memory, which keeps garbage-collection pauses shorter under sustained load — a meaningful advantage in latency-sensitive services running on small containers. axios and got build richer request and response objects, so a process making millions of calls per hour will see more allocation churn. For a worker pinned to a 256 MB container, that difference can decide whether you stay within your memory budget.
Retries, Timeouts & Resilience
A fast client that hangs forever on a dead upstream is worse than a slow one with sane timeouts. Resilience — bounded timeouts, idempotent retries with backoff, and circuit breaking — matters more in production than raw throughput. got ships retries out of the box; axios needs a small plugin; undici and fetch expect you to wire it up, which gives you full control.
Here is a minimal, production-shaped retry wrapper around native fetch using AbortSignal.timeout and exponential backoff with jitter. It only retries idempotent methods and respects a hard deadline:
const RETRIABLE = new Set([408, 429, 500, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function fetchWithRetry(url, opts = {}, { retries = 3, timeoutMs = 5000 } = {}) {
const method = (opts.method || 'GET').toUpperCase();
const idempotent = ['GET', 'HEAD', 'PUT', 'DELETE'].includes(method);
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await fetch(url, { ...opts, signal: AbortSignal.timeout(timeoutMs) });
if (res.ok || !RETRIABLE.has(res.status) || !idempotent || attempt === retries) {
return res;
}
} catch (err) {
if (!idempotent || attempt === retries) throw err;
}
const backoff = Math.min(1000 * 2 ** attempt, 8000);
await sleep(backoff + Math.random() * 250); // jitter avoids thundering herds
}
}These patterns are not optional at scale. If your team is designing resilient service-to-service communication and you want engineers who have shipped this in production, it is worth bringing in specialists — you can hire backend developers who treat timeouts and retries as first-class design concerns rather than afterthoughts.
Which Client Should You Choose?
Use native fetch as your default. It ships with Node.js, has zero dependencies, and is fast enough for the overwhelming majority of services. Drop down to undici's Pool or Client API when a specific component — a gateway, a scraper, a high-fan-out aggregator — needs maximum throughput or fine-grained pool control. Reach for got or axios in application code where their interceptors, retries, and rich ecosystem genuinely save you time, accepting the modest performance cost.
A simple decision rule
If you are starting fresh in 2026: default to fetch, optimize hot paths with undici, and only add a third-party client when you hit a concrete ergonomic need it solves. If you are maintaining an existing axios codebase that performs fine, there is no urgent reason to rip it out — just make sure keep-alive is enabled on its agent.
Picking the right client is the easy part; building a service that uses it well under real production load is harder. If you are building a Node.js system and need experienced engineers, HireNodeJS connects you with pre-vetted senior developers available within 48 hours — without the recruiter overhead.
Hire Expert Node.js Developers — Ready in 48 Hours
Choosing between undici, fetch, axios, and got is a small decision inside a much bigger one: who builds and runs your backend. HireNodeJS.com specialises exclusively in Node.js talent — every developer is pre-vetted on real-world projects, API design, event-driven architecture, and production deployments.
Unlike generalist platforms, our curated pool means you speak only to engineers who live and breathe Node.js. Most clients have their first developer working within 48 hours of getting in touch. Engagements start as short-term contracts and can convert to full-time hires with zero placement fee.
Key Takeaways
In 2026 the smart default for outbound HTTP in Node.js is native fetch: it is fast, dependency-free, and standards-based. When a specific component demands every last request per second, undici's pooled APIs give you the headroom. axios and got remain excellent choices where their ergonomics and ecosystem outweigh the performance difference. Whatever you choose, the real wins come from enabling connection reuse, setting bounded timeouts, and retrying idempotent calls with backoff — the fundamentals matter far more than the logo on your client.
Frequently Asked Questions
What is the best HTTP client for Node.js in 2026?
For most services, native fetch is the best default — it ships with Node.js, has no dependencies, and is built on the fast undici engine. Use undici's Pool API directly for maximum throughput, and axios or got when you want their richer ergonomics.
Is undici faster than axios?
Yes. In throughput benchmarks undici typically handles 50-70% more requests per second than axios, mainly because it pools keep-alive connections aggressively and does less per-request work. The gap narrows over real networks where latency dominates.
Do I still need axios if Node.js has built-in fetch?
Not necessarily. Native fetch covers most use cases. axios is still worth it if you rely on its interceptors, automatic transforms, or its large plugin ecosystem; otherwise fetch plus a small retry helper is usually enough.
How do I add retries to native fetch in Node.js?
Wrap fetch in a loop that retries only idempotent methods on retriable status codes (408, 429, 5xx), using AbortSignal.timeout() for per-attempt deadlines and exponential backoff with jitter between attempts.
Does native fetch reuse connections in Node.js?
Yes. Node.js fetch uses a shared global undici pool, so keep-alive connections are reused automatically. axios and got only reuse connections if you pass an http.Agent with keepAlive enabled.
When should I use undici directly instead of fetch?
Use undici's Client or Pool API when a component needs maximum requests per second or fine-grained control over pool size, pipelining, and timeouts — for example an API gateway, a high-fan-out aggregator, or a large-scale scraper.
Vivek Singh is the founder of Witarist and HireNodeJS.com — a platform connecting companies with pre-vetted Node.js developers. With years of experience scaling engineering teams, Vivek shares insights on hiring, tech talent, and building with Node.js.
Want a Node.js engineer who ships fast, optimised APIs?
HireNodeJS connects you with pre-vetted senior Node.js engineers available within 48 hours. No recruiter fees, no lengthy screening — just top backend talent ready to ship resilient, high-throughput services.
