Retry Pattern: Her Hatada Tekrar Denemek Doğru mu?
Bir istek başarısız olduğunda ilk içgüdü "tekrar dene" olmaktadır. Ama naif bir retry mekanizması sistemi kurtarmak yerine çökertebilir. Bu yazıda Retry Pattern'ı neden ve nasıl doğru uygulamanız gerektiğini anlatıyoruz.

Retry Pattern: Her Hatada Tekrar Denemek Doğru mu?
Bir API isteği başarısız oldu. Ne yaparsın?
İlk içgüdü çoğu zaman şudur: tekrar dene. Kulağa mantıklı gelir. Ama yanlış yapılırsa sistemi kurtarmak yerine çökertebilir.
İki Tür Hata Var
Bir isteğin neden başarısız olduğunu anlamadan retry yapmamalısın.
Geçici hatalar kısa süreliğine oluşur. Ağ paketi düştü, servis o an meşguldü, bağlantı koptu. Birkaç saniye sonra tekrar denersen büyük ihtimalle geçer.
Kalıcı hatalar ise retry yapsan da değişmez. Yanlış URL, eksik yetki, geçersiz veri. Kaç kez denersen dene, aynı hatayı alırsın.
Retry yalnızca geçici hatalar için anlamlıdır.
Saf Retry Neden Tehlikeli?
Şöyle düşün: Servis yoğun yük altında yavaşladı ve hata vermeye başladı. Buna bağlı 100 kullanıcı aynı anda hata aldı ve hepsi hemen retry yaptı. Servis üzerindeki yük iki katına çıktı. Daha fazla hata geldi, daha fazla retry yapıldı, yük daha da arttı.
Bu kısır döngüye Thundering Herd denir. Sistemi tamamen çökertebilir.

Çözüm: Bekleyerek Tekrar Dene
Her retry arasına bir bekleme süresi koy. Ve her seferinde bu süreyi biraz daha uzat. Buna Exponential Backoff denir.
İlk retry 500ms sonra, ikincisi 1 saniye sonra, üçüncüsü 2 saniye sonra gibi.
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxAttempts: number = 3,
baseDelayMs: number = 500
): Promise<T> {
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt >= maxAttempts) throw err;
const delay = baseDelayMs * Math.pow(2, attempt - 1);
await new Promise((res) => setTimeout(res, delay));
}
}
throw new Error("Max attempts reached");
}
Bir Sorun Daha: Herkese Aynı Süre
Exponential backoff iyi bir başlangıçtır ama tek başına yetmez.
Aynı anda hata alan 100 kullanıcı yine aynı anda retry yapar çünkü bekleme süresi herkeste aynıdır. Çözüm her kullanıcıya küçük bir rastgele gecikme eklemektir. Buna Jitter denir.
const jitter = Math.random() * 500; // 0–500ms arası rastgele
const delay = baseDelayMs * Math.pow(2, attempt - 1) + jitter;
Bu küçük ekleme retry trafiğini zaman içine yayar ve Thundering Herd’i önler.

Hangi Hataları Retry Etmeli?
HTTP response kodlarına göre basit bir rehber:
Kod | Retry? | Neden? |
|---|---|---|
429 Too Many Requests | Evet | Rate limit, geçici |
503 Service Unavailable | Evet | Servis meşgul, geçici |
502 / 504 Gateway Timeout | Evet | Ağ sorunu, geçici |
500 Internal Server Error | Belki | Duruma göre değişir |
400 Bad Request | Hayır | Verin hatalı, kalıcı |
401 Unauthorized | Hayır | Auth sorunu, kalıcı |
404 Not Found | Hayır | Kaynak yok, kalıcı |
function isRetryable(statusCode: number): boolean {
return [429, 502, 503, 504].includes(statusCode);
}
Dikkat: Aynı İşlemi İki Kez Yapma
Retry yaparken en sık düşülen tuzak budur.
“Ödeme al” isteği timeout aldı ve retry yaptın. Peki ilk istek aslında sunucuya ulaştı ve işlendi mi? Bilmiyorsun. Retry yaparsan kullanıcıdan iki kez ödeme alabilirsin.
Bu yüzden retry yapılacak işlemlerin idempotent olması gerekir. Yani aynı isteği iki kez göndersen bile sonuç değişmemeli.
Bunun en yaygın yöntemi her isteğe benzersiz bir anahtar eklemektir:
const idempotencyKey = crypto.randomUUID();
await fetch("/api/payment", {
method: "POST",
headers: {
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({ amount: 100 }),
});
Sunucu bu anahtarı görünce “bu isteği daha önce işledim mi?” diye kontrol eder. İşlediyse aynı sonucu döner, tekrar işlemez.
Hepsini Bir Araya Getir
async function retryWithJitter<T>(
fn: () => Promise<T>,
maxAttempts = 3,
baseDelayMs = 500
): Promise<T> {
let attempt = 0;
while (attempt < maxAttempts) {
try {
return await fn();
} catch (err) {
attempt++;
if (attempt >= maxAttempts) throw err;
const exponential = baseDelayMs * Math.pow(2, attempt - 1);
const jitter = Math.random() * baseDelayMs;
const delay = exponential + jitter;
await new Promise((res) => setTimeout(res, delay));
}
}
throw new Error("Max attempts reached");
}
// Kullanım
await retryWithJitter(
() => fetch("/api/payment", { method: "POST" }),
3,
500
);
Özet
Her hatayı retry etme — önce geçici mi kalıcı mı olduğunu anla.
Hemen retry etme — bekleyerek dene (Exponential Backoff).
Herkese aynı süreyi verme — rastgele gecikme ekle (Jitter).
Retry yapacaksan işlemin idempotent olduğundan emin ol.
Servis tamamen çöktüyse retry değil, farklı bir strateji gerekir. Bunu bir sonraki yazıda Circuit Breaker ile ele alacağız.
Summary
This article explains the Retry Pattern in distributed systems and why naive retrying can make failures worse instead of better. It covers the Thundering Herd problem, Exponential Backoff, Jitter, retryable vs non-retryable HTTP errors, and idempotency as a prerequisite for safe retrying.
İlgili İçerikler
Benzer Yazılar
İlgili yazı
Circuit Breaker: Çöken Servisi Defalarca Çağırmayı Bırak
Bir servis çöktüğünde retry yapmak sorunu çözmez, aksine yükü artırır. Circuit Breaker bu döngüyü keser: belirli bir hata eşiği aşılınca bağlantıyı tamamen kapatır, sisteme nefes aldırır ve kendiliğinden iyileşmesine izin verir.
İlgili yazı
Timeout Neden Hayati? Cevap Beklemek Bedava Değil
Bir servis cevap vermiyorsa beklemek masum görünür. Ama o bekleme thread'leri tüketir, belleği doldurur ve tüm sistemi dondurabilir. Timeout'u neden ciddiye almanız gerektiğini ve nasıl doğru kurmanız gerektiğini bu yazıda anlatıyoruz.
Daha yeni
Circuit Breaker: Çöken Servisi Defalarca Çağırmayı Bırak
Daha eski