İçeriğe atla
Ayhan Sipahi Ayhan Sipahi

Axios vs Fetch vs Undici: Node.js HTTP Client Karşılaştırması

Node.js HTTP client'larının kapsamlı karşılaştırması: performans testleri, circuit breaker pattern'ler ve gerçek production deneyimleri

HTTP Client Zorluğu

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. Yanlış varsayılan (örneğin timeout’suz fetch) Lambda concurrency’yi tüketebilir ve altyapı genelinde bir kesintiye yol açabilir. Bu yazı, söz konusu dört client’ı connection pooling, timeout semantiği, circuit-breaker desteği ve pratik production değiş tokuşları açısından karşılaştırıyor.

Yaygın bir sorun: proper timeout handling olmadan native fetch kullanmak. Bu takılan connection’lar Lambda concurrent execution’ları tüketebilir ve infrastructure maliyetlerini etkiler.

Bu deneyim, HTTP client seçiminin sadece feature’larla ilgili olmadığını gösteriyor; production yükü altında neyin bozulduğunu anlamakla ilgili. Doğru client seçimi latency, memory kullanımı ve operasyonel maliyetleri doğrudan etkiler. Bir payment servisi timeout’larında dakikalarca asılı kalan connection’lar tüm Lambda concurrency’yi bloke edebilir; bu da diğer isteklerin de başarısız olmasına yol açar.

Server-Side Client’ların Gizli Bedeli

Browser’da HTTP client’lar basit. Request yaparsın, response’u handle edersin, bitti. Server-side? İşte orada işler ilginçleşiyor:

  • 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

Gelin her büyük oyuncuya bakalım ve production gerçekliğiyle nasıl başa çıktıklarını görelim.

Native Fetch: Her Zaman Yeterli Olmayan Varsayılan

Node.js 18’den beri native fetch var. Her yerde kullanmak cazip - zero dependency, standard API, sevmemek için ne var?

// 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' })
});

Güçlü Yönler

  • 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)

Sınırlamalar

Production’da bizi yakayan şey:

// 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 connection? O yavaşça connection pool’unuzu yiyerek kalabilir. undici tabanlı fetch’te bu daha iyi yönetiliyor olsa da, yüksek concurrency ortamlarında explicit timeout ve connection limit stratejileri şart. Aksi halde “neden process memory sürekli artıyor?” sorusuyla karşılaşırsın.

Production Kararı

Native fetch’i şunlar için kullanın:

  • Basit script’ler ve CLI tool’ları
  • Prototype’lar ve POC’ler
  • Hem client hem server’ı kontrol ettiğinizde

Şunlardan kaçının:

  • Retry, circuit breaker veya connection pooling gerektiğinde
  • Saniyede binlerce request yaparken
  • Güvenilmez third-party API’larla entegre olurken

Axios: İsviçre Çakısı

Axios haftada 45 milyon download ile en popüler seçim olmaya devam ediyor. Her yerde olmasının bir nedeni var.

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;
});

Bilinen Memory Leak

Axios, 502 error’larını handle ederken belirli koşullarda memory leak yapabilir. Sorun follow-redirects dependency’sindedir. Nasıl tespit edileceği:

// 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)
});

Production Kararı

Axios hala şunlar için sağlam:

  • Kompleks request/response transformation’lar
  • Kapsamlı middleware ihtiyacı olduğunda
  • Zaten aşina olan takımlar

Ama şunlara dikkat:

  • Bundle size (1.84MB unpacked, production bundle için ~13KB gzipped)
  • Error response’larla memory leak’ler
  • Connection pooling ekstra setup gerektiriyor. Kurumsal ortamlarda proxy ve sertifika yapılandırması da ekstra kod gerektirir.

Undici: Performans Şampiyonu

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
});

// High-throughput senaryolar için axios'tan 3x daha hızlı
const { statusCode, body } = await request('https://api.example.com', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ data: 'value' }),
  dispatcher: agent
});

Performans Rakamları

1000 concurrent request ile bir payment servisi üzerindeki benchmark sonuçları:

LibraryAvg LatencyP99 LatencyThroughputMemory
Undici23ms89ms4,235 rps124MB
Native Fetch31ms156ms3,122 rps156MB
Axios42ms234ms2,234 rps289MB
Got38ms189ms2,567 rps234MB

Kullandığımız benchmark script’i:

import { performance } from 'perf_hooks';
import { Agent } from 'undici';
import axios from 'axios';
import got from 'got';

const testUrl = 'https://httpbin.org/json';
const concurrency = 100;
const totalRequests = 10000;

// Undici setup
const undiciAgent = new Agent({
  connections: 50,
  pipelining: 10
});

// Axios setup
const axiosClient = axios.create({
  timeout: 5000
});

// Benchmark fonksiyonu
async function benchmark(name: string, clientFn: () => Promise<any>) {
  const start = performance.now();
  const promises = [];
  let completed = 0;
  
  for (let i = 0; i < totalRequests; i++) {
    promises.push(
      clientFn().then(() => completed++)
    );
    
    // Concurrency'yi kontrol et
    if (promises.length >= concurrency) {
      await Promise.race(promises);
      promises.splice(promises.findIndex(p => p.isFulfilled), 1);
    }
  }
  
  await Promise.allSettled(promises);
  const duration = performance.now() - start;
  
  console.log(`${name}: ${Math.round(totalRequests / (duration / 1000))} req/s`);
  console.log(`  Completed: ${completed}/${totalRequests}`);
  console.log(`  Duration: ${Math.round(duration)}ms`);
}

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 birden çok paralel request için önemli performance faydası sağlıyor:

// Benchmark: 50 concurrent request ile HTTP/1.1 vs HTTP/2
const h1Agent = new Agent({ allowH2: false });
const h2Agent = new Agent({ allowH2: true });

// HTTP/1.1: ~200ms ortalama (connection overhead)
// HTTP/2: ~80ms ortalama (multiplexing avantajı)

Gelişmiş Konfigürasyon: Proxy ve Sertifikalar

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
const proxyAgent = new ProxyAgent({
  uri: 'http://proxy.corporate.com:8080',
  auth: 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'
  }
});

// NTLM authentication'lı kurumsal proxy (Windows ortamları)
const ntlmProxyAgent = new ProxyAgent({
  uri: process.env.HTTPS_PROXY,
  token: `NTLM ${Buffer.from(ntlmToken).toString('base64')}`
});

// 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;
  }
}

Production Kararı

Undici şunlarda mükemmel:

  • High-throughput microservice’ler
  • Her milisaniye önemli olduğunda
  • Memory kısıtlı ortamlar

Şunlarda pas geçin:

  • Native HTTP/2 gerekiyorsa
  • Takımınız higher-level abstraction’ları tercih ediyorsa
  • Axios’tan migrate ediyorsanız (çok farklı)

Effect: Fonksiyonel Güç Merkezi

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 benimsenmesi tahmin edilebilir bir eğri izler: 1. haftada kafa karışıklığı, 2. haftada direnç, 4. haftada netlik; type-safe error handling bütün bir runtime bug sınıfını ortadan kaldırır.

// 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))
  })
);

Production Kararı

Effect şunlar için mükemmel:

  • Birden çok failure mode’u olan kompleks business logic
  • Functional programming’e rahat takımlar
  • Type safety kritik olduğunda

İki kere düşünün eğer:

  • Takımınız FP konseptlerine yeni
  • Junior’ları hızlıca onboard etmeniz gerekiyor
  • Basit bir CRUD servisi

Diğerleri: Hızlı Turlar

Got: Node.js Uzmanı

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. Built-in pagination desteği güzel.

Ky: Hafif 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]
  }
});

Minimal overhead ile pilli fetch istediğinizde mükemmel.

SuperAgent: Hala Hayatta

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: Edge Runtime Şampiyonu

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}`
    });
  }
});

// Performans: 402,820 ops/sec vs itty-router'ın 212,598 ops/sec
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.

Enterprise Ortamı: Proxy, Sertifikalar ve Kurumsal Ağlar

Kurumsal ortamda mı çalışıyorsunuz? İşte gerçekten bilmeniz gerekenler:

// Yaygın kurumsal gereksinimler
interface EnterpriseHttpClient {
  proxySupport: boolean;
  ntlmAuth: boolean;
  certificatePinning: boolean;
  mutualTLS: boolean;
  customDNS: boolean;
  socks5Support: boolean;
}

// Gerçek dünya kurumsal proxy setup'ı
import { SocksProxyAgent } from 'socks-proxy-agent';

class EnterpriseHttpClient {
  private agent: any;
  
  constructor() {
    // Birden çok proxy environment variable'ını kontrol et
    const proxyUrl = process.env.HTTPS_PROXY || 
                    process.env.https_proxy || 
                    process.env.HTTP_PROXY || 
                    process.env.http_proxy;
    
    if (proxyUrl) {
      // Farklı proxy tiplerini ele al
      if (proxyUrl.startsWith('socks://')) {
        this.agent = new SocksProxyAgent(proxyUrl);
      } else {
        this.agent = new HttpsProxyAgent(proxyUrl);
      }
      
      // Windows ortamları için NTLM desteği ekle
      if (process.env.NTLM_DOMAIN) {
        this.agent = new NtlmProxyAgent({
          proxy: proxyUrl,
          domain: process.env.NTLM_DOMAIN,
          username: process.env.NTLM_USER,
          password: process.env.NTLM_PASS
        });
      }
    }
  }
  
  async request(url: string, options = {}) {
    // Dahili ve harici URL'leri otomatik algıla ve ele al
    const isInternal = url.includes('.internal.') || 
                      url.includes('.corp.') ||
                      url.startsWith('https://10.') ||
                      url.startsWith('https://192.168.');
    
    if (isInternal) {
      // Dahili URL'ler için proxy'yi atla
      return await fetch(url, {
        ...options,
        agent: undefined
      });
    }
    
    // Harici URL'ler için proxy kullan
    return await fetch(url, {
      ...options,
      agent: this.agent
    });
  }
}

// Kısıtlı ortamlarda sertifika doğrulaması
async function validateCorporateCertificate(cert: any) {
  // Kurumsal sertifika store'una göre kontrol et
  const trustedFingerprints = await fetchFromCorporateVault('/api/trusted-certs');
  
  if (!trustedFingerprints.includes(cert.fingerprint256)) {
    // Güvenlik ekibini uyar
    await notifySecurityTeam({
      event: 'untrusted_certificate',
      fingerprint: cert.fingerprint256,
      hostname: cert.subject.CN
    });
    
    throw new Error('Certificate not in corporate trust store');
  }
}

Kurumsal Proxy Debugging

Kurumsal ortamlardaki yaygın “connection refused” hataları genellikle şunlardan kaynaklanır:

  1. Kurumsal proxy’nin NTLM authentication gerektirmesi
  2. Proxy konfigürasyonunun ortamlar arasında değişmesi
  3. Dahili API’ların yanlışlıkla proxy üzerinden yönlendirilmesi
  4. Proxy’nin belirli header’ları çıkarması

Çözüm: Dahili ve harici URL’leri otomatik algılayan akıllı bir client:

const NO_PROXY_PATTERNS = [
  '*.internal.company.com',
  '10.*',
  '192.168.*',
  'localhost'
];

function shouldUseProxy(url: string): boolean {
  const hostname = new URL(url).hostname;
  return !NO_PROXY_PATTERNS.some(pattern => {
    const regex = new RegExp(pattern.replace('*', '.*'));
    return regex.test(hostname);
  });
}

Circuit Breaker’lar: Production Kurtarıcınız

Hangi HTTP client’ı seçerseniz seçin, circuit breaker ekleyin. İşte Cockatiel ile production setup’ımız:

import { circuitBreaker, retry, wrap, ExponentialBackoff } from 'cockatiel';

// 5 ardışık failure'dan sonra açılan circuit breaker
const breaker = circuitBreaker({
  halfOpenAfter: 10000,
  breaker: new ConsecutiveBreaker(5)
});

// Exponential backoff'lu retry policy
const retryPolicy = retry({
  maxAttempts: 3,
  backoff: new ExponentialBackoff()
});

// Birleştir
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;
  }
);

// Kullanım
try {
  const data = await resilientFetch('https://flaky-api.com/data');
} catch (error) {
  if (breaker.state === 'open') {
    // Circuit açık, fallback kullan
    return getCachedData();
  }
  throw error;
}

Circuit Breaker Production Benefits

Payment provider’da aralıklı timeout’lar olabilir. Circuit breaker olmadan: tüm checkout flow’u bloklanır. Circuit breaker ile: failure threshold’dan sonra anında backup provider’a failover yapılır, gelir kaybı önlenir.

Production Monitoring Setup

Hangi client’ı seçerseniz seçin, instrument edin:

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');

// HTTP client'ınızı wrap edin
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);
  }
}

Karar Matrisi

Kullanım senaryosuna ve ekip bağlamına göre karar matrisi:

Use Caseİlk Tercihİkinci TercihKaçının
High-throughput microservice’lerUndiciGotNative Fetch
Kompleks enterprise API’larAxiosEffectKy
Functional programming takımıEffect-SuperAgent
Basit script’ler/CLI’larNative FetchKyEffect
Browser + Node.jsAxiosKyUndici
Edge computing (Cloudflare)Native FetchHonoNode-specific
Legacy sistem entegrasyonuAxiosSuperAgentEffect

Production Debugging: Pratik Çözümler

Phantom Memory Leak Debugging

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 Exhaustion

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:

// Önce: Binlerce connection ile ölüm
const badClient = axios.create(); // Limitsiz varsayılan agent kullanır

// Sonra: Kontrollü connection kullanımı
const goodClient = axios.create({
  httpAgent: new require('http').Agent({
    keepAlive: true,
    maxSockets: 20,  // Host başına
    maxTotalSockets: 100,  // Toplam
    timeout: 60000,
    keepAliveTimeout: 30000
  }),
  timeout: 10000
});

// Connection monitoring ekle
goodClient.interceptors.response.use(
  (response) => {
    console.log(`Active connections: ${process.env._http_agent?.sockets || 'unknown'}`);
    return response;
  }
);

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;
  }
}

Çıkarımlar

  1. Connection pooling opsiyonel değil - Production’da file descriptor’ları yaktık. Her zaman connection limit’leri konfigüre edin.

  2. Memory leak’ler gerçek - O Axios 502 bug’ı bize haftalarca debugging’e mal oldu. Her zaman error senaryolarıyla load test yapın.

  3. Circuit breaker’lar geliri kurtarır - Her external API fail edecek. Bunun için plan yapın.

  4. Timeout’ların katmanları olmalı - Connection timeout, request timeout, total timeout. Hepsini ayarlayın.

  5. Log’lar yeterli değil - Metrik’lere ihtiyacınız var. Ortalamalar değil, p95/p99 gibi response time percentile’ları önemli. Tracing ile yavaş request’lerin hangi kod path’inden geldiğini bulmak debug süresini ciddi kısaltır.

Değerlendirme

HTTP client manzarası evrim geçirmeye devam ediyor. Native fetch iyileşiyor, undici HTTP/2 ekliyor ve Effect popülerlik kazanıyor. Tavsiyem? Hype’a değil, takımınıza ve use case’inize göre seçin.

Basit başlayın (native fetch), her şeyi ölçün ve gerçek sınırlamalara çarptığınızda upgrade edin. Takımınıza ve kullanım senaryonuza en uygun HTTP client’ı seçin, doğru error handling uygulayın ve her şeyi izleyin. Ne seçerseniz seçin, ihtiyaç duyulmadan önce circuit breaker ekleyin.

Kaynaklar

  • Node.js HTTP Modülü Belgeleri - İstemci istek API’si dahil yerleşik http modülü için resmi Node.js belgeleri
  • undici - GitHub - Node.js yerel fetch API’sinin temeli olan, sıfırdan yazılmış resmi Node.js HTTP/1.1 istemcisi
  • axios Belgeleri - Önleyiciler, örnek yapılandırması ve hata yönetimini kapsayan resmi axios belgeleri
  • got - GitHub - Yeniden deneme, akış ve kancalar için destek içeren Node.js’e özel kullanıcı dostu HTTP istemcisi
  • node-fetch - GitHub - Tarayıcı ve sunucu HTTP kalıplarını birleştirerek Node.js’e Fetch API’sini getiren hafif modül
  • Node.js ile Fetch (undici) - Modern Node.js’te undici destekli yerel fetch API’sini kullanmak için resmi kılavuz

İlgili yazılar