Axios vs Fetch vs Undici: Node.js HTTP Client Karşılaştırması
Node.js sunucudan sunucuya çağrılarda neden varsayılan undici olmalı, Axios, native fetch ve Effect ne zaman daha doğru tercih
Node.js sunucudan sunucuya çağrılar için HTTP client seçimi nadiren tek seferlik bir karar olur: native fetch, Axios, undici ve Effect, bağlantı baskısı, retry fırtınaları ve kısmi başarısızlıklar karşısında birbirinden farklı davranır. Gününü başka servisleri çağırarak geçiren bir servis için varsayılan tercih undici’dir. Kendi connection pool’unu yönetir, timeout’ları açıktır ve native fetch’in zaten üzerinde koştuğu motorun ta kendisidir.
En pahalıya mal olan hata aynı zamanda en kolay üretilen hatadır: timeout’suz native fetch. Asılı kalan bağlantılar Lambda concurrent execution’ları açık tutar; yavaş bir upstream aynı anda hem fatura hem kesinti üretir. Bir client’ın en kötü gününde nasıl davrandığını connection pooling, timeout semantiği ve circuit-breaker desteği belirler; özellik listeleri değil.
Sunucu Tarafında HTTP Client Değiş Tokuşları#
Browser’da HTTP client’lar basit. Request yaparsın, response’u handle edersin, bitti. Sunucu tarafında ise dört özellik API yüzeyinden daha çok önem kazanıyor:
- Connection pooling saniyede binlerce request yaparken kritik hale geliyor
- Memory leak’ler Node.js process’inizi günler içinde yavaşça öldürebilir
- Circuit breaker’lar graceful degradation ile cascading failure arasındaki fark
- Retry stratejileri network probleminin outage’a dönüşüp dönüşmeyeceğini belirler
Dört başlığın her birine her client farklı cevap veriyor ve fark ancak yük altında görünür oluyor.
Native Fetch’in Sınırları ve Değiş Tokuşları#
Node.js 18’den beri native fetch var. Her yerde kullanmak cazip geliyor: sıfır bağımlılık, standart API ve sıfır migration maliyeti.
// Yeterince basit görünüyor
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
Native Fetch’in Parladığı Yerler#
- Zero dependency: Docker image’larınız yalın kalıyor
- Standard API: Aynı kod browser, Node.js, Deno, Bun’da çalışıyor
- Modern: Arka planda undici kullanıyor (Node.js 18’den beri)
Yetersiz Kaldığı Yerler#
Soyutlamanın inceldiği yer timeout’lar:
// Timeout tuzağı - bu düşündüğünüzü yapmıyor
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch('https://slow-api.com', {
signal: controller.signal
});
} catch (error) {
// Bu abort'u yakalar, ama TCP connection hala açık olabilir!
}
AbortController sadece JavaScript tarafını iptal eder. Alttaki TCP bağlantısı açık kalabilir ve connection pool’unuzu yavaş yavaş yiyebilir. undici tabanlı fetch bunu eski polyfill’lerden daha iyi yönetiyor olsa da, yüksek concurrency altında yine de açık timeout ve connection limit ayarları gerekiyor. Bunlar olmadan process memory’si hiçbir heap snapshot’ın açıklamadığı bir nedenle tırmanıyor.
Ne Zaman Yeterli#
Native fetch; basit script’ler, CLI tool’ları, prototype’lar ve hem client hem server’ı aynı ekibin kontrol ettiği durumlar için doğru tercih. Bunun ötesinde, retry, circuit breaker veya connection pooling devreye girdiğinde, saniyede binlerce request’e çıkıldığında ya da güvenilmez bir third-party API söz konusu olduğunda native fetch tek başına yetmemeye başlıyor.
Axios ve Ekosistemi#
Axios, npm’de açık ara en çok indirilen HTTP client olmaya devam ediyor; nedeni de etrafında büyüyen ekosistem.
import axios from 'axios';
import axiosRetry from 'axios-retry';
// Production-ready konfigürasyon
const client = axios.create({
timeout: 10000,
maxRedirects: 5,
validateStatus: (status) => status < 500
});
// Retry logic ekle
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 429; // Rate limited
}
});
// Logging için request/response interceptor'lar
client.interceptors.request.use((config) => {
config.headers['X-Request-ID'] = generateRequestId();
logger.info('Giden request', {
method: config.method,
url: config.url
});
return config;
});
Memory Leak Tespiti#
Axios, 502 error’larını handle ederken memory sızdırabiliyor; sorun çoğu zaman follow-redirects bağımlılığından geliyor. Bu davranışı şöyle görünür kılabilirsiniz:
// Memory leak reprodüksiyon
async function leakTest() {
const promises = [];
for (let i = 0; i < 10000; i++) {
promises.push(
axios.get('https://api.returns-502.com')
.catch(() => {}) // Error object'ler memory'de kalıyordu!
);
}
await Promise.all(promises);
// Heap snapshot'ı burada kontrol et - HTML error response'ları hala memory'de
}
Connection Pooling Düzeltmesi#
Düz Axios her request için yeni connection açar. Scale’de bu server’ınızı öldürür:
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)
});
Bedel gerçek: bundle size (1.84MB unpacked, production bundle için ~13KB gzipped), belirli error response’larla ortaya çıkan memory leak’ler, yukarıdaki connection pooling setup’ı ve kurumsal ağlarda proxy/sertifika yapılandırması için gelen ekstra kod. Buna rağmen Axios; zaten aşina olan takımlar, kompleks request/response transformation’ı gereken durumlar ya da kapsamlı middleware’e dayanan projeler için yerini koruyor.
Undici’yi Fetch Sarmalayıcısı Olmadan Kullanmak#
Undici, Node.js fetch’i içerde güçlendiren şey. Ama direkt kullanmak size süper güçler veriyor.
import { request, Agent } from 'undici';
const agent = new Agent({
connections: 100,
pipelining: 10, // HTTP/1.1 pipelining
keepAliveTimeout: 60 * 1000,
keepAliveMaxTimeout: 600 * 1000
});
// Her çağrıda yeni bağlantı açmak yerine havuzdaki socket'leri yeniden kullanır
const { statusCode, body } = await request('https://api.example.com', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'value' }),
dispatcher: agent
});
Kendi Yükünüzde Ölçmek#
Client sıralaması payload boyutuna, keep-alive ayarlarına, TLS handshake’lerine ve kaç farklı host ile konuştuğunuza göre değişiyor; bu yüzden yayımlanmış rakamlar belirli bir servisle temas ettiğinde nadiren ayakta kalıyor. Bir client’a bağlanmadan önce karşılaştırmayı kendi upstream’inizde çalıştırın:
import { performance } from 'node:perf_hooks';
import { Agent, request } from 'undici';
import axios from 'axios';
import got from 'got';
const testUrl = 'https://httpbin.org/json';
const concurrency = 100;
const totalRequests = 10000;
const undiciAgent = new Agent({ connections: 50, pipelining: 10 });
const axiosClient = axios.create({ timeout: 5000 });
async function benchmark(name: string, clientFn: () => Promise<unknown>) {
const start = performance.now();
let issued = 0;
let completed = 0;
let failed = 0;
// Sabit worker havuzu, böylece concurrency gerçekten sınırlı kalır
const worker = async () => {
while (issued < totalRequests) {
issued++;
try {
await clientFn();
completed++;
} catch {
failed++;
}
}
};
await Promise.all(Array.from({ length: concurrency }, worker));
const duration = performance.now() - start;
console.log(`${name}: ${Math.round(totalRequests / (duration / 1000))} req/s`);
console.log(` Completed: ${completed}, failed: ${failed}`);
console.log(` Duration: ${Math.round(duration)}ms`);
}
// Response body tüketilmezse undici socket'i meşgul tutar
await benchmark('undici', async () => {
const { body } = await request(testUrl, { dispatcher: undiciAgent });
await body.dump();
});
await benchmark('axios', () => axiosClient.get(testUrl));
await benchmark('got', () => got(testUrl).text());
Throughput’un yanında memory ve p99’a da bakın: ortalama latency’de kazanıp p99’da kaybeden bir client genellikle connection pool’unu aç bırakıyordur.
HTTP/2 Desteği#
Undici HTTP/2 destekliyor, ama açıkça aktive edilmesi gerekiyor:
import { Agent, request } from 'undici';
// HTTP/2 aktif agent oluştur
const h2Agent = new Agent({
allowH2: true, // HTTP/2'yi aktive et
connections: 50,
pipelining: 0 // HTTP/2 için pipelining'i deaktive et
});
// Belirli HTTP/2 endpoint'leriyle kullan
const response = await request('https://http2.example.com/api', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ data: 'value' }),
dispatcher: h2Agent
});
// Veya global dispatcher ile
import { setGlobalDispatcher } from 'undici';
setGlobalDispatcher(h2Agent);
// Artık tüm fetch çağrıları mevcut olduğunda HTTP/2 kullanıyor
const h2Response = await fetch('https://http2.example.com/data');
HTTP/2, paralel isteklerin çoğu aynı host’a gittiğinde işe yarıyor; çünkü havuzdaki socket’ler için sıraya girmek yerine tek bir bağlantıyı paylaşıyorlar:
// Geçmeden önce ikisini de kendi endpoint'inizde çalıştırın
const h1Agent = new Agent({ allowH2: false });
const multiplexedAgent = new Agent({ allowH2: true });
// Fark tek bir host'a giden concurrency arttıkça açılıyor,
// çok sayıda host'a dağıldıkça kapanıyor; orada zaten havuz kazanıyor.
Proxy ve Sertifika Konfigürasyonu#
Undici production ortamları için kapsamlı proxy ve sertifika yönetimi sunar:
import { ProxyAgent, Agent } from 'undici';
import { readFileSync } from 'fs';
// Authentication'lı proxy konfigürasyonu.
// `token` değeri Proxy-Authorization header'ına olduğu gibi yazılır.
const proxyAgent = new ProxyAgent({
uri: 'http://proxy.corporate.com:8080',
token: `Basic ${Buffer.from('username:password').toString('base64')}`,
requestTls: {
ca: readFileSync('./ca.pem'),
cert: readFileSync('./client-cert.pem'),
key: readFileSync('./client-key.pem'),
rejectUnauthorized: true
}
});
// Self-signed veya dahili CA'lar için özel sertifika yönetimi
const secureAgent = new Agent({
connect: {
ca: [
readFileSync('./root-ca.pem'),
readFileSync('./intermediate-ca.pem')
],
cert: readFileSync('./client-cert.pem'),
key: readFileSync('./client-key.pem'),
// Certificate pinning
checkServerIdentity: (hostname, cert) => {
const expectedFingerprint = 'AA:BB:CC:DD:EE:FF...';
const actualFingerprint = cert.fingerprint256;
if (actualFingerprint !== expectedFingerprint) {
throw new Error(`Certificate fingerprint mismatch for ${hostname}`);
}
},
servername: 'api.internal.company.com', // SNI
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3'
}
});
// Sertifika hatalarında retry ile kullanım
async function secureRequest(url: string, options = {}) {
try {
return await request(url, {
...options,
dispatcher: secureAgent
});
} catch (error) {
if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
console.error('Certificate verification failed:', error);
// Fallback mantığı veya alert
}
throw error;
}
}
Buradaki istisna NTLM. Handshake’i request’e değil TCP bağlantısına bağlı olduğu için karşılaştırılan client’ların hiçbiri bunu tek başına tamamlayamıyor. Bu çağrıları cntlm ya da px gibi yerel bir proxy köprüsüne yönlendirin; client köprüyle düz Basic auth konuşsun.
Higher-level abstraction’ları tercih eden, geniş bir middleware ekosistemine ihtiyaç duyan ya da Axios’tan migrate olan takımlar bu ekstra API yüzeyinden vazgeçebilir; çünkü iki API birbirinden yeterince farklı ve bu geçiş kolay bir dönüştürme değil. Bunların hiçbiri geçerli değilse undici’nin karmaşıklığı karşılığını veriyor: high-throughput microservice’ler, memory kısıtlı ortamlar ve her milisaniyenin önemli olduğu yerler.
Effect’in Tipli Hata Kanalı#
Effect tamamen farklı bir yaklaşım benimsiyor. Promise’ler yerine, built-in error handling’li composable effect’ler alıyorsunuz.
import { Effect, Schedule, Duration } from 'effect';
import { HttpClient, HttpClientError } from '@effect/platform';
// Otomatik retry'lı API client tanımla
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);
});
Öğrenme Eğrisi#
Effect, karşılığını vermeden önce ciddi bir yatırım istiyor. Generator’lar ve pipe’lar oturana kadar sözdizimi yabancı kalıyor. Kazanç sonra geliyor: hatalar tip imzasının içinde yaşadığı için bir sürü runtime sürprizi compile hatasına dönüşüyor.
// Effect öncesi: Runtime sürprizleri
async function riskyOperation() {
try {
const user = await fetchUser();
const orders = await fetchOrders(user.id); // Fail edebilir
return processOrders(orders); // Bu da fail edebilir
} catch (error) {
// Network mi? Auth mi? Business logic mi? Kim bilir!
logger.error('Bir şeyler fail etti', error);
}
}
// Effect ile: Error'lar type'ın parçası
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))
})
);
Yatırımın Karşılığını Verdiği Yer#
Takımınız FP konseptlerine yeniyse, junior’ları hızlıca onboard etmeniz gerekiyorsa ya da ortada basit bir CRUD servisi varsa bu ölçekte yatırımın karşılığı çıkmıyor. Bunların dışında, özellikle birden çok failure mode’u olan kompleks business logic’te, functional programming’e zaten aşina takımlarda ya da type safety’nin kritik olduğu yerlerde Effect, öğrenme maliyetini fazlasıyla karşılıyor.
Diğerleri: Kısa Kısa#
Got#
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 denemesi ${retryCount}`, error.message);
}]
}
});
Sadece Node.js projeleri için harika. Pagination yardımcıları, got.stream ile streaming ve isteğe bağlı DNS cache hazır geliyor.
Ky#
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]
}
});
Minimal ek yükle, hazır gelen bir fetch istediğinizde ideal.
SuperAgent Hâlâ Kullanımda#
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 hala çalışıyor
});
Plugin sistemi güçlü, ama Axios popülerlik yarışını kazandı.
Hono#
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
const app = new Hono();
// Cloudflare Workers gibi edge ortamları için yapıldı
app.post('/proxy', async (c) => {
const { url, method = 'GET', headers, body } = await c.req.json();
try {
// Arka planda web standard fetch kullanır
const response = await fetch(url, {
method,
headers: {
...headers,
'User-Agent': 'Hono-Proxy/1.0'
},
body: method !== 'GET' ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(10000) // 10s timeout
});
// Verimlilik için response'u stream et
return new Response(response.body, {
status: response.status,
headers: response.headers
});
} catch (error) {
throw new HTTPException(502, {
message: `Upstream error: ${error.message}`
});
}
});
export default app;
Bundle size ve cold start süresinin en önemli olduğu Cloudflare Workers, Vercel Edge Functions ve diğer edge runtime’lar için mükemmel.
Proxy, Sertifikalar ve Kurumsal Ağlar#
Kurumsal ağlar, hiçbir client’ın varsayılan olarak karşılamadığı gereksinimler ekliyor: kimlik doğrulamalı bir egress proxy, dahili bir CA, client sertifikaları ve proxy’yi tamamen atlaması gereken host listesi. Undici bu dördünü de dispatcher’lar üzerinden karşılıyor:
import { Agent, ProxyAgent } from 'undici';
import { readFileSync } from 'node:fs';
const proxyUrl =
process.env.HTTPS_PROXY ??
process.env.https_proxy ??
process.env.HTTP_PROXY ??
process.env.http_proxy;
// Dahili trafik: kurumsal root CA ve client sertifikası, proxy yok
const directDispatcher = new Agent({
connections: 50,
connect: {
ca: readFileSync('./corporate-root-ca.pem'),
cert: readFileSync('./client-cert.pem'),
key: readFileSync('./client-key.pem')
}
});
// Doğrudan çıkış: Node'un kendi CA paketi, client sertifikası yok
const publicDispatcher = new Agent({ connections: 50 });
// Harici trafik: proxy tanımlıysa her şey egress proxy üzerinden çıkar
const proxyDispatcher = proxyUrl
? new ProxyAgent({ uri: proxyUrl })
: publicDispatcher;
const INTERNAL_SUFFIXES = ['.internal.company.com', '.corp.company.com'];
const INTERNAL_PREFIXES = ['10.', '192.168.', '127.'];
function dispatcherFor(url: string) {
const { hostname } = new URL(url);
const isInternal =
hostname === 'localhost' ||
INTERNAL_SUFFIXES.some((suffix) => hostname.endsWith(suffix)) ||
INTERNAL_PREFIXES.some((prefix) => hostname.startsWith(prefix));
return isInternal ? directDispatcher : proxyDispatcher;
}
export function enterpriseFetch(url: string, options: RequestInit = {}) {
// Native fetch `dispatcher` alır; eski `agent` seçeneği sessizce yok sayılır
return fetch(url, { ...options, dispatcher: dispatcherFor(url) } as RequestInit);
}
Herkese açık host’lar kendi dispatcher’ını istiyor. connect.ca verdiğinizde Node’un gömülü kök sertifika deposuna ekleme yapmıyorsunuz, onun yerini alıyorsunuz. Doğrudan çıkışta dahili dispatcher’ı kullanırsanız herkese açık her endpoint sertifika doğrulamasında takılır. Client sertifikası da isteyen her sunucuya gider.
Asıl tuzak agent seçeneği. Node’un native fetch’i onu uyarı vermeden yok sayıyor; makine ağın içinde olduğu için testlerde “çalışan” bir proxy ayarı, doğrudan egress’i olmayan bir container’da ilk çalıştırmada patlıyor.
Kurumsal Proxy Debugging#
Kurumsal ortamlardaki yaygın “connection refused” hataları genellikle şunlardan kaynaklanır:
- Kurumsal proxy’nin NTLM authentication gerektirmesi
- Proxy konfigürasyonunun ortamlar arasında değişmesi
- Dahili API’ların yanlışlıkla proxy üzerinden yönlendirilmesi
- Proxy’nin belirli header’ları çıkarması
İkinci ve üçüncü maddeyi yukarıdaki dispatcherFor ortadan kaldırıyor: bypass listesi, her ortamın farklı yazdığı bir NO_PROXY metninde değil kodun içinde duruyor. Eşleştirmeyi parse edilmiş hostname üzerinde yapın. URL’nin bir parçasına göre eşleştirmek, api-10.example.com gibi herkese açık bir hostun sessizce 10. özel aralık kuralına takılıp proxy’yi atlamasına yol açar.
Production’da Circuit Breaker’lar#
Hangi HTTP client’ı seçerseniz seçin, circuit breaker ekleyin. Cockatiel ile bağlantı kodu kısa kalıyor:
import {
circuitBreaker,
retry,
wrap,
handleAll,
handleWhen,
BrokenCircuitError,
ConsecutiveBreaker,
ExponentialBackoff,
CircuitState
} from 'cockatiel';
import { request } from 'undici';
// 5 ardışık failure'dan sonra açılır, 10 saniye sonra upstream'i tekrar yoklar
const breaker = circuitBreaker(handleAll, {
halfOpenAfter: 10_000,
breaker: new ConsecutiveBreaker(5)
});
// Açık circuit'in reddi retry edilebilir bir hata değil
const retryPolicy = retry(
handleWhen((error) => !(error instanceof BrokenCircuitError)),
{ maxAttempts: 3, backoff: new ExponentialBackoff() }
);
// Retry breaker'ın dışında kalır; circuit açıldığında denemeler durur
const resilient = wrap(retryPolicy, breaker);
async function fetchWithFallback(url: string) {
try {
return await resilient.execute(async () => {
const response = await request(url);
if (response.statusCode >= 500) {
throw new Error(`Server error: ${response.statusCode}`);
}
return response;
});
} catch (error) {
if (breaker.state === CircuitState.Open) {
return getCachedData();
}
throw error;
}
}
wrap içindeki sıra önemli: en soldaki policy en dışta çalışır. Breaker’ı retry’ın dışına koyarsanız, tek bir mantıksal çağrı, breaker’ın zaten çöktüğünü bildiği bir upstream’e üç deneme birden harcar. Sözün diğer yarısını retry filtresi taşıyor. Düz handleAll ile açık circuit’in reddi de retry edilebilir bir hata sayılır; çağrı, fallback’e sıra gelmeden önce backoff programının tamamını yürür.
Circuit Breaker Ne Kazandırır#
Bir payment provider aralıklı timeout vermeye başladığında her checkout isteği onun arkasında sıraya giriyor ve akışın tamamı tıkanıyor. Breaker, eşik aşıldığı anda bunu hızlı bir başarısızlığa çeviriyor; yedek provider’a geçmeyi ya da cache’lenmiş yanıt dönmeyi mümkün kılan da bu, çünkü breaker olmadan hiçbir çağrı geri dönmüyor ve fallback hiç çalışmıyor.
Production İzleme Kurulumu#
Hangi client’ı seçerseniz seçin, instrument edin:
import { metrics, trace, SpanStatusCode } from '@opentelemetry/api';
const meter = metrics.getMeter('http-client');
const tracer = trace.getTracer('http-client');
const requestDuration = meter.createHistogram('http.client.duration', {
description: 'HTTP client request duration in milliseconds'
});
const activeRequests = meter.createUpDownCounter('http.client.active_requests', {
description: 'In-flight HTTP client requests'
});
export async function instrumentedRequest(url: string, options: RequestInit = {}) {
const method = options.method ?? 'GET';
const { hostname } = new URL(url);
const attributes: Record<string, string | number> = { method, hostname };
const span = tracer.startSpan(`HTTP ${method}`, { attributes });
const start = Date.now();
activeRequests.add(1, { method, hostname });
try {
const response = await fetch(url, {
...options,
headers: { ...options.headers, 'X-Trace-ID': span.spanContext().traceId }
});
attributes.status_code = response.status;
span.setAttribute('http.response.status_code', response.status);
return response;
} catch (error) {
attributes.error_type = (error as NodeJS.ErrnoException).code ?? 'unknown';
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
requestDuration.record(Date.now() - start, attributes);
activeRequests.add(-1, { method, hostname });
span.end();
}
}
Süreyi gauge yerine histogram’a yazın ve p95 ile p99 olarak okuyun. Ortalamalar connection pool açlığını neredeyse kusursuz gizliyor: çağrıların yüzde 95’ini anında karşılayıp kalanını saniyelerce bekleten bir havuz yine de sağlıklı bir ortalama üretiyor. Tablonun diğer yarısı in-flight sayacı; socket’ler havuza geri verilmemeye başladığında yükseliyor ve orada kalıyor.
Karar Matrisi#
Kullanım senaryosuna ve ekip bağlamına göre karar matrisi:
| Use Case | İlk Tercih | İkinci Tercih | Kaçının |
|---|---|---|---|
| High-throughput microservice’ler | Undici | Got | Native Fetch |
| Kompleks enterprise API’lar | Axios | Effect | Ky |
| Functional programming takımı | Effect | - | SuperAgent |
| Basit script’ler/CLI’lar | Native Fetch | Ky | Effect |
| Browser + Node.js | Axios | Ky | Undici |
| Edge computing (Cloudflare) | Native Fetch | Hono | Node-specific |
| Legacy sistem entegrasyonu | Axios | SuperAgent | Effect |
Pratik Production Debugging#
Görünmez Memory Leak’ler#
Servisler günler boyunca heap dump’larda belirgin bir iz bırakmadan yavaşça memory tüketebilir. Yaygın bir neden error handling’deki ince bug’lardır:
// Memory leak - görebilir misiniz?
const pendingRequests = new Map();
async function makeRequest(id: string, url: string) {
const controller = new AbortController();
pendingRequests.set(id, controller);
try {
const response = await fetch(url, {
signal: controller.signal
});
return response;
} catch (error) {
// BUG: Başarılı veya abort edilen request'leri asla temizlemiyoruz!
if (error.name === 'AbortError') {
throw error;
}
throw error;
} finally {
// Bu en baştan burada olmalıydı
pendingRequests.delete(id);
}
}
Ders: Request takibini her zaman temizleyin, error path’lerinde bile.
Connection Pool Tükenmesi#
Yüksek trafik olayları, servisler 502 dönmeye başladığında connection pool sınırlamalarını ortaya çıkarabilir. Sorun genellikle varsayılan connection limit’lerine dayanır:
import { Agent as HttpAgent } from 'node:http';
// Önce: binlerce connection ile ölüm
const badClient = axios.create(); // Varsayılan agent, socket limiti yok
// Sonra: kontrollü connection kullanımı
const httpAgent = new HttpAgent({
keepAlive: true,
keepAliveMsecs: 30_000,
maxSockets: 20, // Host başına
maxTotalSockets: 100, // Tüm host'lar toplamında
timeout: 60_000
});
const goodClient = axios.create({ httpAgent, timeout: 10_000 });
// Socket sayıları environment'tan değil agent'ın kendisinden okunur
const countSockets = (pool: NodeJS.ReadOnlyDict<unknown[]>) =>
Object.values(pool).reduce((total, sockets) => total + (sockets?.length ?? 0), 0);
setInterval(() => {
console.warn(
`sockets in use: ${countSockets(httpAgent.sockets)}, ` +
`idle: ${countSockets(httpAgent.freeSockets)}`
);
}, 10_000).unref();
Yavaş İstek Analizi#
Aşağıdaki gibi bir request analyzer, yavaş istek hatalarının büyük bölümünü ortadan kaldırır:
class RequestAnalyzer {
private static slowRequests = new Map();
static trackRequest(url: string, options: RequestInit) {
const requestId = Math.random().toString(36);
const start = Date.now();
// Yavaş request'ler için request stack trace'ini takip et
const stack = new Error().stack;
this.slowRequests.set(requestId, {
url,
method: options.method || 'GET',
start,
stack: stack?.split('\n').slice(2, 8).join('\n') // Caller context'ini al
});
// 30 saniye sonra otomatik temizlik
setTimeout(() => {
const req = this.slowRequests.get(requestId);
if (req) {
const duration = Date.now() - req.start;
if (duration > 5000) {
console.warn(`Slow request detected after cleanup:`, {
...req,
duration,
possibleHang: duration > 30000
});
}
this.slowRequests.delete(requestId);
}
}, 30000);
return requestId;
}
static completeRequest(requestId: string, response?: Response, error?: Error) {
const req = this.slowRequests.get(requestId);
if (!req) return;
const duration = Date.now() - req.start;
if (duration > 1000) { // 1s üzeri request'leri logla
console.warn(`Slow request completed:`, {
...req,
duration,
status: response?.status,
error: error?.message,
// Bu, yavaş request'i kodunuzun hangi kısmının yaptığını belirlemeye yardım eder
callerStack: req.stack
});
}
this.slowRequests.delete(requestId);
}
}
// Herhangi bir HTTP client ile kullanım
async function trackedFetch(url: string, options: RequestInit = {}) {
const requestId = RequestAnalyzer.trackRequest(url, options);
try {
const response = await fetch(url, options);
RequestAnalyzer.completeRequest(requestId, response);
return response;
} catch (error) {
RequestAnalyzer.completeRequest(requestId, undefined, error as Error);
throw error;
}
}
Varsayılanınızı Seçmek#
Sunucudan sunucuya trafikte kazanç şu: sahiplendiğiniz bir connection pool, söylediği şeyi yapan timeout’lar ve yazdığınız konfigürasyon ile socket’in gerçekte yaptığı şey arasında en dar fark. Bu varsayılanı şu durumlarda değiştirin: çağıran kod Axios interceptor’larına ihtiyaç duyuyorsa ya da browser ile aynı kod tabanını paylaşıyorsa, ekip zaten Effect kullanıyor ve HTTP’yi de aynı tipli hata kanalının içinde istiyorsa, veya servis günde birkaç çağrı yapıyor ve AbortSignal.timeout ile native fetch gerçekten yetiyorsa.
Hangi client kazanırsa kazansın iki ayar yerinde kalır. Timeout’lar katmanlı olmalı: connect, request başına ve toplam geçen süre için ayrı değerler. Varsayılanlar gözden geçirilmeli; çoğu client, binlerce socket’i açık tutan bir servis için değil script’ler için ayarlanmış limitlerle geliyor. Circuit breaker’ı upstream hâlâ sağlıklıyken ekleyin; olayın ortasında kurmaya çalışmak çok daha kötü bir deneyim.
Kaynaklar#
- Node.js HTTP Modülü Belgeleri (yeni sekmede açılır) - Agent socket limitleri ve client request API’si dahil, yerleşik http modülünün resmi Node.js belgeleri
- undici - GitHub (yeni sekmede açılır) - Node.js native fetch’in temelini oluşturan, sıfırdan yazılmış resmi Node.js HTTP/1.1 client’ı; Agent, ProxyAgent ve dispatcher API’leri burada
- axios Belgeleri (yeni sekmede açılır) - Interceptor’lar, instance konfigürasyonu ve error handling’i kapsayan resmi axios belgeleri
- got - GitHub (yeni sekmede açılır) - Retry, stream ve hook desteği olan, Node.js’e özel kullanıcı dostu HTTP client’ı
- node-fetch - GitHub (yeni sekmede açılır) - Browser ve sunucu HTTP pattern’lerini birleştirerek Node.js’e Fetch API’sini getiren hafif modül
- Node.js ile Fetch (undici) (yeni sekmede açılır) - Modern Node.js’te undici destekli native fetch API’sini kullanmak için resmi kılavuz
- Cockatiel - GitHub (yeni sekmede açılır) - Circuit breaker, retry ve bulkhead policy’lerinin
wrapile nasıl birleştirileceğini gösteren resmi dokümantasyon - OpenTelemetry JavaScript API (yeni sekmede açılır) - Histogram, counter ve span oluşturma için resmi instrumentation kılavuzu
İlgili yazılar
API, ödeme ve mesaj tüketicisi geliştirenler için idempotency'ye pratik giriş: HTTP metot semantiği, idempotency key'leri, upsert ve yaygın tuzaklar.
idempotency · api-design · distributed-systems +4
Bir Lambda filosu Middy'nin statik middleware modelini ne zaman aşar, projeye özel bir motor istek başına konfigürasyonu nasıl çözer, bakımı neye mal olur
lambda · middleware · performance +6
Tek bir backend üzerinde çalışan web SPA ve mobil uygulama için uzun süreli işlere dair tek bir varsayılan desen ve onu geçersiz kılmanız gereken durumlar.
api-design · real-time · webhooks +4
Single Table Design'da DynamoDB throttling'i önleme ve yönetme: partition key tasarımı, write sharding, kapasite modları, DAX caching ve retry pattern'leri.
dynamodb · aws · reliability +4
Effect'i adım adım öğrenmek ve AWS Lambda ile entegre etmek için pratik bir rehber: gerçek kod örnekleri, yaygın hatalar ve üretim desenleri.
typescript · functional-programming · lambda +4