Background job queue’lar modern web uygulamasının omurgası. Email gönderimi, rapor üretimi, third-party API çağrısı, image processing gibi async iş queue’ya düşer, worker’lar tarafından işlenir. Kurması kolay, ölçeklendirmesi hata dolu.
6 farklı projede Redis (BullMQ), RabbitMQ, AWS SQS kullandım. Her seferinde yeni bir kenar durum öğrendim. Bu yazıda kaçırdığım, öğrenince düzelttiğim 5 kritik konu.
1. Poison message’ı yalıtın
İlk büyük prodüksiyon kazasında bir email gönderim job’ı sonsuz retry’a girdi. Email adresi bozuktu, SMTP her defasında aynı hatayı döndü, queue retry with exponential backoff yaptı ama eventually give up etmedi. 4 saatte 15.000 retry birikti, Redis memory’si doldu, worker’lar diğer job’ları işleyemedi, sistem yarı çalıştı.
Poison message: belirli retry sonrası başarılamadığı garanti olan mesaj. Bunları dead letter queue’ya (DLQ) taşımak şart:
- Max retry sayısını belirle (genelde 3-5 arası)
- Retry sayısını aşan job’ı ayrı bir queue’ya at
- DLQ’yu ayrıca gözlemle (alarm kur), ama worker pool’a etki etmesin
BullMQ’da attempts: 5 ve backoff yapılandırılıyor, attempt bitince failed state’e geçiyor. Ben bunların üzerine custom DLQ ekleyip ops ekibinin incelemesi için birikmesini sağlıyorum.
2. Retry storm’dan kaçının
Downstream bir servis çöktüğünde, onlarca worker aynı anda failed job’ları retry etmeye başlar. Bu retry storm çökmüş servisi ayakta kalkmasını engeller, aynı zamanda queue’yu doldurur.
Çözüm: circuit breaker + jittered backoff.
Circuit breaker: downstream hata oranı belirli eşiği geçince worker direkt fail eder, retry yapmaz. 30 saniye sonra half-open moduna geçip test eder. Sağlıklıysa kapalı’ya döner.
Jittered backoff: retry interval’ına rastgele jitter ekleyin (örn. 100ms ± 50ms), tüm worker’ların aynı anda retry etmesi önlenir. Thunder herd problemi çözülür.
const delay = Math.min(30000, 1000 * Math.pow(2, attempt)) + Math.random() * 500;3. Priority queue’lar zorunlu, tek queue yetmez
İlk projelerde tek queue kullandım, tüm job’ları aynı havuza attım. Critical job (user’ın 2FA SMS’i) non-critical (haftalık rapor email) arkasında bekliyordu.
Priority queue şart:
- High priority: user-facing, latency-sensitive (SMS, login email, payment confirmation)
- Normal priority: genel iş (bildirim, webhook)
- Low priority: arka plan (raporlama, cleanup, analytics aggregate)
BullMQ’da priority option ile tek queue’da önceliklendirme var. RabbitMQ’da priority queue farklı setup. SQS’de queue başına ayrı worker pool.
Worker pool da önceliğe göre ayrılmalı. High priority queue’nun kendi worker’ı olmalı, low priority işlerden etkilenmemeli.
4. At-least-once vs exactly-once
Queue sistemleri genelde at-least-once garanti veriyor. Bir mesaj 1 veya birden fazla kere işlenebilir. Ama kod çoğunlukla exactly-once varsayıyor.
Örnek bug: credit card charge job’ı at-least-once işlendi, kullanıcı iki kere charge oldu. Feedback: çok üzücü.
Çözüm: idempotency. Her job unique bir idempotency key ile işlensin, job başında “bu key daha önce işlendi mi?” check’i yapılsın, işlendiyse no-op.
async function processPayment(job) {
const key = `payment:${job.data.orderId}:${job.data.attemptId}`;
const already = await redis.get(key);
if (already) return JSON.parse(already);
const result = await chargeCard(job.data);
await redis.setex(key, 86400, JSON.stringify(result));
return result;
}Idempotency key stratejisi proje başında kurulmalı. Sonradan eklemek işe yarar ama dekopore edilmiş kodu değiştirmek zor.
5. Observability: queue depth ve processing time
Queue sistemi monitoring olmadan çalışmıyor. En azından şunlar lazım:
- Queue depth (her queue için): kaç job bekliyor
- Processing time p50 / p95 / p99: job ne kadar sürüyor
- Failure rate: başarısız olan job yüzdesi
- Age of oldest job: en eski job ne kadar önce kuyruğa girdi
Alarm eşikleri:
- Queue depth > 10.000 (veya sizin için anlamlı limit) → worker scale up
- p95 processing time > threshold → worker performance problemi var
- Failure rate > %5 → downstream veya kod sorunu
- Oldest job age > 10 dakika (high priority queue için) → worker pool yetersiz
BullMQ Arena, RabbitMQ Management UI, SQS CloudWatch metrics bu verileri veriyor. Ama dashboard kurup alarm oluşturmak ek iş.
Bonus: graceful shutdown
Worker deploy edilirken yarı işlenmiş job’lar durumunda ne olmalı? Worker sigterm alınca:
- Yeni job almayı bırak
- Üzerinde çalışan job’u bitirmeye çalış (timeout’lu)
- Bitmezse job’u queue’ya geri koyup (requeue) çık
Bu pattern olmadan deploy her seferinde yarım iş bırakıyor. BullMQ’da gracefulShutdown implementation, RabbitMQ’da prefetch = 1 ve manual ack discipline.
Kararlılık her zaman performans’tan önce
Queue scale up etmek kolay, 100 worker’a çıkar, throughput artar. Ama kararlılık öncelikli:
- Poison message DLQ’ya gitsin
- Retry storm circuit breaker ile durdurulsun
- Priority queue’lar ayrılsın
- Idempotency kabul edilsin
- Observability gün 1’den olsun
Bu 5 prensip olmadan queue sistemi bir bomba. Çalışırken sessiz, arıza olunca gürültülü.
6 projeden sonra öğrendiğim ders: queue sistemi “kur unut” değil, “kur izle düzenle” disiplini. Production’da queue incident’ları startup’ların en sık outage nedeni.