Bir Express rotası içinde resim boyutlandırma işlemi çalıştırmak felakete davetiye çıkarmaktır. Bir kullanıcı on megabaytlık bir fotoğraf yükler, sunucunuz pikselleri işlemekle uğraşmaya başlar ve otuz saniye sonra istek zaman aşımına uğrar. Arka plan iş kuyrukları tam olarak bu tür acıları önlemek için vardır. Node.js ekosisteminde Bull ve BullMQ, Redis aracılığıyla asenkron işleri yönetmek için iki ağır top haline gelmiştir. DNA'ları benzer olsa da felsefe ve günlük kullanım ergonomisi açısından keskin bir şekilde ayrılırlar. Doğru olanı seçmek önemlidir, çünkü daha sonra değiştirmek basit bir paket güncellemesi değildir.
Ortak Temel
Her iki kütüphane de omurga olarak Redis kullanır. Redis; atomik işlemleri, geciktirilmiş işler için sıralı kümeleri (sorted sets) ve olaylar için pub/sub mekanizmasını yönetir. Eğer halihazırda önbelleğe alma veya oturum yönetimi için Redis kullanıyorsanız, bir iş kuyruğu eklemek yeni bir altyapı gerektirmez. Hem Bull hem de BullMQ; öncelikleri, backoff ile yeniden denemeleri, eşzamanlılık kontrollerini ve tekrarlanabilir işleri destekler. Bu örtüşme, seçimi kolaylaştırmak yerine zorlaştırır. Sadece bir özellik listesine güvenemezsiniz. Bunun yerine, her bir kütüphanenin kodunuzu nasıl yapılandırmanızı istediğine bakmanız gerekir.
Bull: Deneyimli Bir Veteran
Bull yıllardır piyasadadır ve binlerce üretim uygulamasında çalışmaktadır. İşini yapar. API, her şeyi tek bir Queue örneği (instance) içinde paketler. Onu örneklendirir, bir işleme fonksiyonu tanımlar ve tüm olayları aynı nesne üzerinden dinlersiniz. Bu monolitik tasarım, eski Node.js kalıplarından geliyorsanız tanıdık gelecektir. Yaygın async/await kullanımından önceki kod tabanları Bull ile doğal bir uyum içindedir, çünkü Bull, callback'ler ve eski Redis istemcileriyle birlikte büyümüştür.
Dezavantajı ise sıkı bağımlılıktır (tight coupling). API sunucunuz bir iş oluşturduğunda, işçi (worker) mantığını içeren aynı Queue nesnesini içe aktarır. Pratikte bu, web sürecinizin hiç çalıştırmayacağı bağımlılıkları da beraberinde getirmesi anlamına gelir. Bu ölümcül bir kusur değildir ancak temiz mimari açısından rahatsız edicidir. Basit iş yükleri için bunu hiç fark etmeyebilirsiniz. Ancak onlarca modülü olan büyük ekiplerde bu sürtünme birikir.
BullMQ: Sıfırdan Yeniden İnşa
BullMQ resmi halefidir. İlk günden itibaren TypeScript ile yeniden yazılmıştır, bu nedenle tipler JavaScript kaynak koduna sonradan eklenmiş bir parça değildir. API, sorumlulukları farklı sınıflara (classes) böler. Queue işlerin eklenmesini yönetir. Worker bunların işlenmesini yönetir. QueueEvents ise gözlemlenebilirliği (observability) yönetir. Bu ayrım, modern dağıtık sistemlerin gerçekte nasıl çalıştığını yansıtır. API podlarınızın yalnızca Queue sınıfına ve bir Redis bağlantısına ihtiyacı vardır. Worker podlarınız ise Worker sınıfını içe aktarır. Sınır sadece kavramsal değil, fizikseldir.
Bu değişim büyük ekiplerde meyvesini verir. Yeni bir özellik sunan bir geliştirici, işlemcinin (processor) hangi dosyada olduğunu bilmeden bir işi kuyruğa ekleyebilir. Derleyici, iş verileri ile işleyiciler arasındaki tip uyumsuzluklarını çalışma zamanında değil, erkenden yakalar. async/await API'si de modern Node.js'de yerel (native) bir his verir. Kendinizi eski geleneklerle savaşırken bulmazsınız.
İş Akışları: Geçici Çözümlerden Birinci Sınıf Vatandaşlara
Çok adımlı iş akışları, iki kütüphane arasındaki en büyük farkı ortaya koyar.
Bir e-ticaret faturalandırma hattı kurduğunuzu varsayalım. Bir müşteri ödeme yapar. Envanteri rezerve etmeniz, karttan ücret çekmeniz, bir PDF oluşturmanız ve bir e-posta göndermeniz gerekir. Bull ile bu adımları zincirlemek, manuel kayıt tutmak anlamına gelir. Bir işlemci bir sonraki işi başlatabilir, durumu Redis üzerinden veya hantal veri yükleri aracılığıyla aktarabilir. Ebeveyn-çocuk koordinasyonunu kendiniz yazarsınız. Çalışır, ta ki çalışmayana kadar. Yeniden deneme mantığı karmaşıklaşır. Eğer PDF adımı başarısız olursa, ödemeyi geri almak, hata yapmaya müsait özel bir telafi kodu gerektirir.
BullMQ, FlowProducer'ı tanıtır. Ebeveynlerin çocuklarını otomatik olarak beklediği bir iş ağacı tanımlarsınız. Faturalandırma örneğinde, finalize-order adlı bir kök iş oluşturursunuz ve bunun üç çocuğu olur: reserve-inventory, charge-payment ve generate-pdf. E-posta bildirimini PDF işinin bir çocuğu yapabilirsiniz. Redis, grafik yapısını saklar. Ebeveyn, yalnızca tüm bağımlılıklar başarılı olduğunda etkinleşir. Eğer bir çocuk başarısız olursa, tüm dal durur. Polling döngüleri veya özyinelemeli (recursive) iş başlatıcılar yazmanıza gerek kalmaz. Bu sadece bir sözdizimsel şeker (syntactic sugar) değildir; iş mantığını nasıl modellediğinizi değiştirir.
Hız Sınırlama: Kaba Bir Araç mı, Yoksa Hassas Bir Neşter mi?
Her iki kütüphane de işlem hacmini sınırlayabilir, ancak hassasiyet dereceleri muazzam ölçüde farklıdır.
Bull applies rate limits per queue. If you set a queue to process one hundred jobs per second, that ceiling covers every job in the queue equally. This is fine for homogeneous workloads. It breaks down in multitenant SaaS platforms. Imagine one noisy customer dumping a million webhook deliveries into a shared queue. Bull's queue-level limit means you cannot slow that tenant without slowing everyone else. Your options are ugly. Spin up separate Redis queues per customer and manage them dynamically, or accept the unfairness.
BullMQ adds group-based rate limiting. You tag each job with a group key, typically a tenant or user ID, and define limits per group. The same queue processes jobs for all tenants, but the scheduler throttles each group independently. A burst from Customer A does not starve Customer B. You avoid queue sprawl and keep your Redis keyspace tidy. For platforms with noisy-neighbor concerns, this alone can justify the migration.
Cleaner Architecture in Practice
The separation of Queue and Worker is subtle until you debug a production incident. With Bull, it is common to see job creation code deep in route handlers that also import heavy processing dependencies. BullMQ forces you to decide where work happens. Your web servers stay lean. Your worker containers bundle the heavy libraries, image processors, or headless browsers. If a memory leak appears, you know exactly which process type to profile. The mental model is closer to systems like Celery or Sidekiq.
Making the Choice
Start with BullMQ if you are laying fresh tracks. The TypeScript definitions are accurate and complete. Job flows eliminate reams of orchestration code. Group rate limiting solves fairness problems before they start. The async/await API feels native. There is little reason to choose the older library for a greenfield project.
Stay on Bull if it is already working. Migrations cost time and risk stability. If your jobs are flat and independent, you are not missing features you actually need. A queue that sends password reset emails and resizes avatars does not need flow graphs. Rewriting working code for theoretical purity is not engineering. It is hobbyism.
Migration Reality Check
If you do switch, treat it as an infrastructure change, not a code refactor. Bull and BullMQ use different Redis key schemas. They cannot read each other's job data or state. You cannot flip a feature flag and hope old jobs finish. You must drain every existing queue to zero, deploy the new workers, and start enqueueing with BullMQ. Plan for a maintenance window or a blue-green deployment where old workers consume the legacy queue while new workers handle the new one.
