Node Fetch vs Got vs Undici: Throughput Under Real TLS Churn

Node Fetch vs Got vs Undici: Throughput Under Real TLS Churn
17 views
Microbenchmarks that hit http://127.0.0.1 lie. Production fan-out pays for TLS handshakes, DNS, idle pool eviction, and slow peers. Global fetch (undici under the hood), Got, and raw undici Client/Pool/Agent behave differently once you churn connections — and the wrong default creates keep-alive storms or socket starvation.
⚡ TL;DR: Use undici Agent/Pool with explicit connections, pipelining, and timeouts for high-QPS egress; use fetch for simple app code with a shared dispatcher; keep Got only where its hooks/retry ecosystem still earns its keep. Benchmark with TLS + multiple hosts. See also HTTP/2 in Node Behind ALB for protocol-level cousins of these failures.

Node Fetch vs Got vs Undici: production guidance

Surface Pooling Retries Hooks Best fit
fetch via dispatcher DIY limited app-level calls
Got yes built-in rich legacy scripts
undici Pool/Agent first-class DIY low-level platforms / gateways
typescript
// src/http/egress.ts
import { Agent, fetch as undiciFetch } from "undici";

// ✅ One shared agent per process — sized for fan-out
export const egressAgent = new Agent({
 connections: 32,
 pipelining: 1, // keep 1 unless you know the peer is safe
 keepAliveTimeout: 30_000,
 keepAliveMaxTimeout: 60_000,
 connect: { timeout: 5_000, rejectUnauthorized: true },
 bodyTimeout: 30_000,
 headersTimeout: 30_000,
});

export function apiFetch(url: string, init?: RequestInit) {
 return undiciFetch(url, { ...init, dispatcher: egressAgent } as any);
}
typescript
// ❌ New Agent/Got instance per request — TLS churn amplifier
export const bad = (url: string) => fetch(url); // default agent OK
export const worse = async (url: string) => {
 const { default: got } = await import("got");
 return got(url); // still OK-ish
};
export const worst = (url: string) =>
 undiciFetch(url, { dispatcher: new Agent() } as any);

Benchmark recipe that matches prod

Drive traffic at multiple hostnames with forced connection limits and TLS. Measure p50/p99 latency, RSS, and sockets in TIME_WAIT.
typescript
// scripts/bench-egress.ts
import { performance } from "node:perf_hooks";
import { apiFetch } from "../src/http/egress.js";

async function run(name: string, n: number, concurrency: number, url: string) {
 const lat: number[] = [];
 let i = 0;
 async function worker() {
 while (i < n) {
 const k = i++;
 const t0 = performance.now();
 const res = await apiFetch(url);
 await res.arrayBuffer();
 lat.push(performance.now() - t0);
 }
 }
 await Promise.all(Array.from({ length: concurrency }, () => worker()));
 lat.sort((a, b) => a - b);
 console.log(name, { p50: lat[Math.floor(n * 0.5)], p99: lat[Math.floor(n * 0.99)] });
}

Retries and idempotency

Got’s automatic retries are convenient and dangerous on POST. undici/fetch force you to be explicit — that is a feature for platforms.
typescript
export async function getWithRetry(url: string, attempts = 3) {
 let last: unknown;
 for (let i = 0; i < attempts; i++) {
 try {
 const res = await apiFetch(url);
 if (res.status >= 500) throw new Error(`upstream_${res.status}`);
 return res;
 } catch (err) {
 last = err;
 await new Promise((r) => setTimeout(r, 50 * 2 ** i));
 }
 }
 throw last;
}

Choosing defaults for a platform team

  1. Ban per-request dispatchers in lint.
  2. Export one named agent for internal mesh, one for public egress with stricter timeouts.
  3. Migrate Got call sites that only use got.get → fetch + shared agent.
  4. Keep Got where beforeRetry / pagination hooks are load-bearing — until replaced deliberately.

Closing checklist

✅ Dos
– ✅ Share one undici Agent/Pool per trust domain
– ✅ Benchmark with real TLS and multiple hosts
– ✅ Cap connections to protect upstreams
– ✅ Make retries explicit and method-aware
– ✅ Track pool stats / connect errors in metrics ❌ Don’ts
– ❌ Don’t trust localhost HTTP benchmarks for library choice
– ❌ Don’t enable pipelining blindly against unknown peers
– ❌ Don’t auto-retry non-idempotent methods
– ❌ Don’t create Got/Agent instances in hot paths
– ❌ Don’t ignore DNS + TLS in p99 budgets

Related reading

Last updated on September 11, 2026 Related: Node undici Dispatcher Pools: Kill Keep-Alive Storms Under Spikes; Node.js Event Loop Blocking: The Silent API Killer and How to Fix It; Node.js Streams Backpressure: The Memory Leak That Kills Servers at 3AM.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

1 Comment

Leave a comment

No account needed. Name and email are optional.