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 undiciAgent/Poolwith explicitconnections,pipelining, and timeouts for high-QPS egress; usefetchfor 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 inTIME_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
- Ban per-request dispatchers in lint.
- Export one named agent for internal mesh, one for public egress with stricter timeouts.
- Migrate Got call sites that only use
got.get→fetch+ shared agent. - 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
- HTTP/2 in Node Behind ALB: Multiplexing Pitfalls That Reset Streams
- Node SDK Generation: OpenAPI-Driven Clients LLMs Cannot Hallucinate
- Lambda + Bedrock: Stream Tokens Without API Gateway Caps
- Node.js Event Loop Lag: Catch P99 Stalls
Watch: 100 Days of AI on YouTube
Short videos from CheatCoders, one AI engineering topic per day.
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Claude Code vs Cursor vs GitHub Copilot: Choose by Workflow, and Set Up One Repo That Works With All Three
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
- Python String Methods: Every str Method With Real Production Examples
- Css Animation Performance: Performance-Optimized Motion for Modern UIs
Newly added
- AWS Systems Manager Parameter Store: Hierarchical Runtime Config for Coding Agents
- Amazon Cognito Identity Pools: Federate Coding-Agent Tool Callers Without Long-Lived Keys
- Amazon Athena for Coding Agents: Query Tool Audit Logs and Eval Traces on S3 Without a Warehouse
- AWS Batch for Coding Agents: Parallel Overnight PR Reviews and Long-Running Jobs Without Lambda Timeouts
- Amazon Bedrock Prompt Management: Version and A/B Test Coding-Agent System Prompts
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.

Pingback: Node Blob and File APIs: Efficient Multipart Upload Paths to S3 - CheatCoders