Node.js HTTP Clients in 2026: undici, fetch, axios & got
product-development11 min readintermediate

Node.js HTTP Clients in 2026: undici, fetch, axios & got

Vivek Singh
Founder & CEO at Witarist · June 14, 2026

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.

Feature comparison matrix of undici, native fetch, axios and got for Node.js HTTP clients
Figure 1 — Capability matrix: throughput, built-in status, pooling, retries, interceptors, streaming, and mocking across the four clients.

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.

Figure 2 — Interactive throughput benchmark: requests per second by client across payload sizes.

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.

http-pool.js
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.

Diagram of how undici pools keep-alive connections between a Node.js app and an origin API
Figure 3 — undici routes requests through a pool of reusable keep-alive sockets instead of opening a new connection per call.
Ready to build your team?

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.

Figure 4 — Interactive radar comparison scoring each client across throughput, ergonomics, features, ecosystem, and footprint.

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.

⚠️Warning
Synthetic benchmarks against localhost exaggerate client differences because there is no network latency to hide behind. Over a real network, DNS, TLS, and round-trip time often dominate, shrinking the gap. Always benchmark against a realistic target before optimizing your client choice.

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:

fetch-with-retry.js
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.

🚀Pro Tip
Reach for AbortSignal.timeout() instead of manual setTimeout/clearTimeout dance — it produces a composable signal you can also combine with AbortSignal.any() to honor both a per-attempt timeout and an overall request deadline.

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.

💡Tip
🚀 Ready to scale your Node.js team? HireNodeJS.com connects you with pre-vetted engineers who can join within 48 hours — no lengthy screening, no recruiter fees. Browse developers at hirenodejs.com/hire

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.

Topics
#Node.js#undici#fetch#axios#got#HTTP client#performance#backend

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.

About the Author
Vivek Singh
Founder & CEO at Witarist

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.

Developers available now

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.