Node.js’te AbortController ile Zaman Aşımı, İptal ve Kaynak Sızıntılarını Önleme
Node.js’te AbortController ile HTTP, stream ve DB işlemlerini iptal edip timeout’ları standartlaştırın.
Node.js uygulamalarında performans sorunlarının “gizli” sebeplerinden biri, aslında artık ihtiyaç olmayan işler: kullanıcı sayfadan çıktı ama istek hâlâ devam ediyor, upstream servis geç dönüyor, stream bitmiyor… Sonuç: açık socket’ler, gereksiz CPU, bellek sızıntıları.
Node 18+ ile gelen AbortController, bu problemi “tek bir iptal sinyali” ile çözmek için güçlü bir standart.
1) Temel fikir: Tek sinyal, çok iş
AbortController size bir signal verir. Bu sinyal:
fetchgibi API’lerde isteği iptal eder- Event listener’larla kendi kodunuzda iptal akışını tetikler
- Stream’leri sonlandırma/temizleme için kullanılabilir
const controller = new AbortController();
const { signal } = controller;
setTimeout(() => controller.abort(new Error('timeout')), 1500);
signal.addEventListener('abort', () => {
console.log('İptal edildi:', signal.reason);
});
2) fetch için gerçek bir timeout (Node 18+)
fetch doğası gereği “timeout” parametresi sunmaz. AbortController ile bunu standartlaştırabilirsiniz.
async function fetchWithTimeout(url, { timeoutMs = 2000, ...opts } = {}) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(new Error('Fetch timeout')), timeoutMs);
try {
const res = await fetch(url, { ...opts, signal: controller.signal });
return res;
} finally {
clearTimeout(id);
}
}
// kullanım
const res = await fetchWithTimeout('https://httpbin.org/delay/3', { timeoutMs: 1000 });
Neden iyi? Çünkü iptal, sadece beklemeyi değil bağlantıyı da keserek kaynak tüketimini azaltır.
3) Express’te istemci bağlantısı kopunca işi durdurmak
Klasik senaryo: Kullanıcı sayfayı kapattı, ama siz hâlâ pahalı bir upstream çağrısı yapıyorsunuz.
import express from 'express';
const app = express();
app.get('/report', async (req, res) => {
const controller = new AbortController();
// istemci bağlantısı kapanırsa iptal et
req.on('close', () => controller.abort(new Error('Client disconnected')));
try {
const r = await fetch('https://httpbin.org/delay/5', { signal: controller.signal });
const data = await r.json();
res.json({ ok: true, data });
} catch (err) {
if (controller.signal.aborted) {
// burada genelde response yazmazsınız; socket gitmiş olabilir
return;
}
res.status(500).json({ ok: false, error: String(err) });
}
});
app.listen(3000);
Bu yaklaşım, özellikle rapor üretimi, proxy/aggregator endpoint’ler, LLM çağrıları gibi uzun süren işlemlerde fark yaratır.
4) Birden fazla işi tek timeout ile yönetmek (fan-out)
Bir endpoint içinde 3 farklı servise istek attığınızı düşünün. Hepsini aynı “bütçe” ile sınırlandırın.
async function withDeadline(deadlineMs, fn) {
const c = new AbortController();
const t = setTimeout(() => c.abort(new Error('Deadline exceeded')), deadlineMs);
try {
return await fn(c.signal);
} finally {
clearTimeout(t);
}
}
const result = await withDeadline(1200, async (signal) => {
const [a, b, c] = await Promise.all([
fetch('https://httpbin.org/delay/1', { signal }).then(r => r.json()),
fetch('https://httpbin.org/delay/2', { signal }).then(r => r.json()),
fetch('https://httpbin.org/delay/1', { signal }).then(r => r.json()),
]);
return { a, b, c };
});
Burada deadline aşılırsa hepsi iptal olur; “yarım kalmış ama soketi açık” işler birikmez.
5) Kendi async işinizi iptal edilebilir yapmak
AbortController sadece fetch için değil; kendi kodunuzda da iptal bayrağı olarak çalışır.
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(resolve, ms);
if (signal) {
const onAbort = () => {
clearTimeout(id);
reject(signal.reason ?? new Error('Aborted'));
};
if (signal.aborted) return onAbort();
signal.addEventListener('abort', onAbort, { once: true });
}
});
}
async function expensiveWork(signal) {
for (let i = 0; i < 10; i++) {
await sleep(200, signal); // her adımda iptal kontrolü
}
return 'done';
}
Pratik öneriler
- Timeout’ları “her katmanda” ayrı ayrı koymak yerine tek bir deadline belirleyin.
- İptal geldiğinde mutlaka temizlik yapın: timer’ları
clearTimeout, listener’larıonce, açık kaynakları kapatın. signal.reasonile hatayı anlamlı taşıyın (timeout mı, client disconnect mi?).
AbortController’ı doğru kullanmak, Node.js servislerinde hem maliyeti düşürür hem de tail latency’yi iyileştirir. Küçük bir araç gibi görünür; ama üretimde büyük fark yaratır.