Server-Side HTTP Clients: Von Native Fetch bis Effect, eine Production-Perspektive
Ein umfassender Vergleich von Node.js HTTP Clients mit Performance-Benchmarks, Circuit Breaker Patterns und echten Production-Geschichten
Der HTTP Client Fehler, der uns $50K kostete#
Vor drei Jahren lief unsere Microservices-Architektur ganz gut. Siebenundzwanzig Services, alle plaudern fröhlich über HTTP. Dann kam Black Friday, und unser Payment-Service begann zu timen out. Nicht zu failen—nur zu hängen. Für 30 Sekunden. Bei jeder Request.
Der Übeltäter? Wir verwendeten native fetch ohne proper Timeout-Handling. Diese hängenden Connections verbrauchten alle unsere Lambda Concurrent Executions. AWS-Rechnung in diesem Monat: $50K über Budget. Autsch.
Diese teure Lektion lehrte mich, dass die Wahl eines HTTP Clients nicht nur um Features geht—es geht darum zu verstehen, was um 3 Uhr morgens kaputt geht, wenn dein On-Call-Telefon klingelt.
Warum Server-Side HTTP Clients wichtiger sind als du denkst#
Im Browser sind HTTP Clients straightforward. Du machst eine Request, handelst die Response, fertig. Server-side? Da wird's interessant:
- Connection Pooling wird kritisch, wenn du tausende Requests pro Sekunde machst
- Memory Leaks können deinen Node.js-Prozess über Tage langsam töten
- Circuit Breakers bedeuten den Unterschied zwischen graceful degradation und cascading failures
- Retry-Strategien bestimmen, ob ein Netzwerk-Blip zu einem Ausfall wird
Lass uns in jeden großen Player eintauchen und sehen, wie sie mit der Production-Realität umgehen.
Native Fetch: Der Default, der nicht immer genug ist#
Seit Node.js 18 haben wir native fetch. Es ist verlockend, es überall zu verwenden—zero dependencies, standard API, was gibt's nicht zu lieben?
// Sieht einfach genug aus
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
Wo Native Fetch glänzt#
- Zero dependencies: Deine Docker-Images bleiben schlank
- Standard API: Derselbe Code funktioniert in Browser, Node.js, Deno, Bun
- Modern: Basiert unter der Haube auf undici (seit Node.js 18)
Wo es zu kurz kommt#
Das hat uns in Production gebissen:
// Die Timeout-Falle - das macht nicht, was du denkst
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://slow-api.com', {
signal: controller.signal
});
} catch (error) {
// Das fängt den Abort, aber die TCP-Connection könnte noch offen sein!
}
Der AbortController bricht nur die JavaScript-Seite ab. Die darunterliegende TCP-Connection? Die könnte bleiben und langsam deinen Connection Pool auffressen.
Production-Urteil#
Verwende native fetch für:
- Einfache Scripts und CLI-Tools
- Prototypes und POCs
- Wenn du sowohl Client als auch Server kontrollierst
Vermeide es wenn:
- Du Retries, Circuit Breakers oder Connection Pooling brauchst
- Tausende Requests pro Sekunde machst
- Mit flaky Third-Party-APIs integrierst
Axios: Das Schweizer Taschenmesser#
Axios bleibt mit 45 Millionen wöchentlichen Downloads die populärste Wahl. Es gibt einen Grund, warum es überall ist.
import axios from 'axios';
import axiosRetry from 'axios-retry';
// Production-ready Konfiguration
const client = axios.create({
timeout: 10000,
maxRedirects: 5,
validateStatus: (status) => status <500
});
// Retry-Logic hinzufügen
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429; // Rate limited
}
});
// Request/Response Interceptors für Logging
client.interceptors.request.use((config) => {
config.headers['X-Request-ID'] = generateRequestId();
logger.info('Ausgehende Request', {
method: config.method,
url: config.url
});
return config;
});
Das Memory Leak, das wir fanden#
Letztes Jahr entdeckten wir, dass Axios Memory leakte, wenn es 502-Errors handelte. Das Problem war in der follow-redirects
Dependency. So haben wir es aufgespürt:
// Memory Leak Reproduktion
async function leakTest() {
const promises = [];
for (let i = 0; i <10000; i++) {
promises.push(
axios.get('https://api.returns-502.com')
.catch(() => {}) // Error-Objekte blieben im Memory!
);
}
await Promise.all(promises);
// Check Heap Snapshot hier - HTML Error Responses noch im Memory
}
Connection Pooling Fix#
Plain Axios öffnet eine neue Connection pro Request. Bei Scale killt das deinen Server:
import Agent from 'agentkeepalive';
const keepAliveAgent = new Agent({
maxSockets: 100,
maxFreeSockets: 10,
timeout: 60000,
freeSocketTimeout: 30000
});
const client = axios.create({
httpAgent: keepAliveAgent,
httpsAgent: new Agent.HttpsAgent(keepAliveAgent.options)
});
Production-Urteil#
Axios ist immer noch solide für:
- Komplexe Request/Response Transformationen
- Wenn du extensive Middleware brauchst
- Teams, die bereits damit vertraut sind
Aber pass auf bei:
- Bundle Size (1.84MB unzipped)
- Memory Leaks mit Error Responses
- Connection Pooling erfordert Extra-Setup
Undici: Der Performance-Champion#
Undici ist das, was Node.js fetch intern antreibt. Aber es direkt zu verwenden gibt dir Superkräfte.
import { request, Agent } from 'undici';
const agent = new Agent({
connections: 100,
pipelining: 10, // HTTP/1.1 pipelining
keepAliveTimeout: 60 * 1000,
keepAliveMaxTimeout: 600 * 1000
});
// 3x schneller als axios für High-Throughput-Szenarien
const { statusCode, body } = await request('https://api.example.com', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'value' }),
dispatcher: agent
});
Die Performance-Zahlen#
Wir haben Benchmarks auf unserem Payment-Service durchgeführt (1000 concurrent requests):
Library | Avg Latency | P99 Latency | Throughput | Memory
-------------|-------------|-------------|------------|--------
Undici | 23ms | 89ms | 4,235 rps | 124MB
Native Fetch | 31ms | 156ms | 3,122 rps | 156MB
Axios | 42ms | 234ms | 2,234 rps | 289MB
Got | 38ms | 189ms | 2,567 rps | 234MB
HTTP/2 Support#
Undici unterstützt HTTP/2, aber es muss explizit aktiviert werden:
import { Agent, request } from 'undici';
// Agent mit aktiviertem HTTP/2 erstellen
const h2Agent = new Agent({
allowH2: true, // HTTP/2 aktivieren
connections: 50,
pipelining: 0 // Pipelining für HTTP/2 deaktivieren
});
// Mit spezifischen HTTP/2 Endpoints verwenden
const response = await request('https://http2.example.com/api', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'value' }),
dispatcher: h2Agent
});
// Oder mit globalem Dispatcher
import { setGlobalDispatcher } from 'undici';
setGlobalDispatcher(h2Agent);
// Jetzt verwenden alle fetch-Aufrufe HTTP/2 wenn verfügbar
const h2Response = await fetch('https://http2.example.com/data');
HTTP/2 bringt erhebliche Performance-Vorteile für mehrere parallele Requests:
// Benchmark: HTTP/1.1 vs HTTP/2 mit 50 gleichzeitigen Requests
const h1Agent = new Agent({ allowH2: false });
const h2Agent = new Agent({ allowH2: true });
// HTTP/1.1: ~200ms Durchschnitt (Connection Overhead)
// HTTP/2: ~80ms Durchschnitt (Multiplexing Vorteil)
Production-Urteil#
Undici exzelliert bei:
- High-Throughput Microservices
- Wenn jede Millisekunde zählt
- Memory-beschränkte Umgebungen
Skip es wenn:
- Du native HTTP/2 brauchst
- Dein Team höhere Abstraktionen bevorzugt
- Du von Axios migrierst (zu unterschiedlich)
Effect: Das funktionale Kraftpaket#
Effect verfolgt einen komplett anderen Ansatz. Statt Promises bekommst du composable Effects mit eingebautem Error Handling.
import { Effect, Schedule, Duration } from 'effect';
import { HttpClient, HttpClientError } from '@effect/platform';
// Definiere deinen API Client mit automatischen Retries
const apiClient = HttpClient.HttpClient.pipe(
HttpClient.retry(
Schedule.exponential(Duration.seconds(1), 2).pipe(
Schedule.jittered,
Schedule.either(Schedule.recurs(3))
)
),
HttpClient.filterStatusOk
);
// Type-safe Error Handling
const fetchUser = (id: string) =>
Effect.gen(function* (_) {
const response = yield* _(
apiClient.get(`/users/${id}`),
Effect.catchTag('HttpClientError', (error) => {
if (error.response?.status === 404) {
return Effect.succeed({ found: false });
}
return Effect.fail(error);
})
);
return yield* _(response.json);
});
Die Lernkurven-Story#
Wir haben Effect einem Team vorgestellt. Woche 1: Verwirrung. Woche 2: Frustration. Woche 4: "Wir gehen nie wieder zurück." Das type-safe Error Handling eliminierte eine ganze Klasse von Bugs.
// Vor Effect: Runtime-Überraschungen
async function riskyOperation() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id); // Könnte failen
return processOrders(orders); // Könnte auch failen
} catch (error) {
// Ist es Network? Auth? Business Logic? Wer weiß!
logger.error('Irgendwas ist gefailed', error);
}
}
// Mit Effect: Errors sind Teil des Types
const safeOperation = Effect.gen(function* (_) {
const user = yield* _(fetchUser);
const orders = yield* _(fetchOrders(user.id));
return yield* _(processOrders(orders));
}).pipe(
Effect.catchTags({
NetworkError: (e) => logAndRetry(e),
AuthError: (e) => refreshTokenAndRetry(e),
ValidationError: (e) => Effect.fail(new BadRequest(e))
})
);
Production-Urteil#
Effect ist perfekt für:
- Komplexe Business Logic mit mehreren Failure Modes
- Teams, die mit funktionaler Programmierung vertraut sind
- Wenn Type Safety kritisch ist
Denk zweimal nach wenn:
- Dein Team neu bei FP-Konzepten ist
- Du Juniors schnell onboarden musst
- Es ein simpler CRUD-Service ist
Die Anderen: Schnelle Runden#
Got: Der Node.js-Spezialist#
import got from 'got';
const client = got.extend({
timeout: { request: 10000 },
retry: {
limit: 3,
methods: ['GET', 'PUT', 'DELETE'],
statusCodes: [408, 429, 500, 502, 503, 504],
errorCodes: ['ETIMEDOUT', 'ECONNRESET'],
calculateDelay: ({ attemptCount }) => attemptCount * 1000
},
hooks: {
beforeRetry: [(error, retryCount) => {
logger.warn(`Retry Versuch ${retryCount}`, error.message);
}]
}
});
Großartig für Node.js-only Projekte. Eingebauter Pagination-Support ist neat.
Ky: Der leichtgewichtige Fetch-Wrapper#
import ky from 'ky';
const api = ky.create({
prefixUrl: 'https://api.example.com',
timeout: 10000,
retry: {
limit: 2,
methods: ['get', 'put', 'delete'],
statusCodes: [408, 429, 500, 502, 503, 504]
}
});
Perfekt wenn du fetch mit Batterien aber minimalem Overhead willst.
SuperAgent: Noch am Leben#
import superagent from 'superagent';
superagent
.post('/api/users')
.send({ name: 'John' })
.retry(3, (err, res) => {
if (err) return true;
return res.status >= 500;
})
.end((err, res) => {
// Callback Style funktioniert noch
});
Plugin-System ist mächtig, aber Axios hat den Popularitätswettbewerb gewonnen.
Circuit Breakers: Dein Production-Lebensretter#
Egal welchen HTTP Client du wählst, füg einen Circuit Breaker hinzu. Hier ist unser Production Setup mit Cockatiel:
import { circuitBreaker, retry, wrap, ExponentialBackoff } from 'cockatiel';
// Circuit Breaker der nach 5 konsekutiven Failures öffnet
const breaker = circuitBreaker({
halfOpenAfter: 10000,
breaker: new ConsecutiveBreaker(5)
});
// Retry Policy mit exponential backoff
const retryPolicy = retry({
maxAttempts: 3,
backoff: new ExponentialBackoff()
});
// Kombiniere sie
const resilientFetch = wrap(
retryPolicy,
breaker,
async (url: string) => {
const response = await undici.request(url);
if (response.statusCode >= 500) {
throw new Error(`Server error: ${response.statusCode}`);
}
return response;
}
);
// Verwendung
try {
const data = await resilientFetch('https://flaky-api.com/data');
} catch (error) {
if (breaker.state === 'open') {
// Circuit ist offen, verwende Fallback
return getCachedData();
}
throw error;
}
Circuit Breaker rettete unseren Black Friday#
Wahre Geschichte: Payment Provider hatte intermittierende 30-Sekunden-Timeouts. Ohne Circuit Breaker: gesamter Checkout-Flow blockiert. Mit Circuit Breaker: nach 5 Failures, sofortiges Failover zum Backup-Provider. Geretteter Umsatz: $2M.
Production Monitoring Setup#
Welchen Client du auch wählst, instrumentiere ihn:
import { metrics } from '@opentelemetry/api-metrics';
const meter = metrics.getMeter('http-client');
const requestDuration = meter.createHistogram('http.request.duration');
const requestCount = meter.createCounter('http.request.count');
// Wrappe deinen HTTP Client
async function instrumentedRequest(url: string, options: any) {
const start = Date.now();
const labels = { method: options.method, url: new URL(url).hostname };
try {
const response = await yourHttpClient(url, options);
labels.status = response.status;
labels.success = 'true';
return response;
} catch (error) {
labels.status = error.response?.status || 0;
labels.success = 'false';
throw error;
} finally {
requestDuration.record(Date.now() - start, labels);
requestCount.add(1, labels);
}
}
Die Entscheidungsmatrix#
Nach Jahren Production-Erfahrung, hier meine Empfehlungsmatrix:
Use Case | Erste Wahl | Zweite Wahl | Vermeide |
---|---|---|---|
High-throughput Microservices | Undici | Got | Native Fetch |
Komplexe Enterprise APIs | Axios | Effect | Ky |
Functional Programming Team | Effect | - | SuperAgent |
Einfache Scripts/CLIs | Native Fetch | Ky | Effect |
Browser + Node.js | Axios | Ky | Undici |
Edge Computing (Cloudflare) | Native Fetch | Hono | Node-specific |
Legacy System Integration | Axios | SuperAgent | Effect |
Auf die harte Tour gelernte Lektionen#
-
Connection Pooling ist nicht optional - Wir haben in Production File Descriptors verbrannt. Konfiguriere immer Connection Limits.
-
Memory Leaks sind real - Dieser Axios 502 Bug kostete uns Wochen Debugging. Teste immer mit Error-Szenarien.
-
Circuit Breakers retten Umsatz - Jede externe API wird failen. Plan dafür.
-
Timeouts brauchen Schichten - Connection Timeout, Request Timeout, Total Timeout. Setz sie alle.
-
Logs sind nicht genug - Du brauchst Metriken. Response Time Percentiles, nicht Durchschnitte.
Was kommt als Nächstes?#
Die HTTP Client Landschaft entwickelt sich weiter. Native fetch wird besser, undici fügt HTTP/2 hinzu, und Effect gewinnt an Zugkraft. Mein Rat? Wähle basierend auf deinem Team und Use Case, nicht Hype.
Fang einfach an (native fetch), miss alles, und upgrade wenn du echte Limitationen triffst. Und was auch immer du wählst, füg Circuit Breakers hinzu bevor du sie brauchst. Vertrau mir dabei.
Happy fetching, und mögen deine APIs immer 200 OK zurückgeben! 🚀
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!