Approval & comparisons

BullMQ vs RabbitMQ in 2026: Which Fits Scheduled Jobs?

BullMQ vs RabbitMQ compared on delayed jobs, cron schedules, retries and rate limits, plus what BullMQ's Postgres backend and RabbitMQ 4.4 changed in 2026.

Prefer it in screens? See this guide as a Web Story

Quick answer

BullMQ is a job queue library that runs on Redis or, since version 6, PostgreSQL, with delayed jobs, cron schedulers, retries with backoff and rate limits built in. RabbitMQ is a message broker that routes messages between services in any language. For background and scheduled jobs inside an app, pick BullMQ. For moving events between services, pick RabbitMQ.

BullMQ and RabbitMQ end up in the same search because both put work on a queue, but they sit at different layers. BullMQ is a library you import: your code adds jobs and your workers process them, and the queue state lives in a datastore you already run. RabbitMQ is a server you operate: producers publish messages to exchanges, the broker routes them to queues, and any client that speaks AMQP consumes them. If the job is "run this function later, retry it if it fails, and do it again every weekday at 9am", BullMQ does that out of the box. If the job is "fan this order event out to billing, email and analytics, written in three languages", that is what RabbitMQ was built for.

Two releases in 2026 made most older comparisons out of date. BullMQ 6.0, released on July 30, 2026, added a PostgreSQL backend, so "BullMQ requires Redis" is no longer true. RabbitMQ 4.4, released on October 20, 2026, lets quorum queues delay messages natively, and the RabbitMQ team no longer maintains the old delayed-message plugin. This comparison uses the current docs for both and looks at the part most posts skip: jobs that have to run at a specific time. If you are designing that part from scratch, our guide to job queue architecture for scheduled posts covers claiming, leases and recovery in depth.

PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.

01

What is the difference between BullMQ and RabbitMQ?

The BullMQ docs describe it as a distributed job and message queue with SDKs for TypeScript/JavaScript (Node.js and Bun), Python, Elixir, Rust, .NET and PHP. All of them share the same Lua and SQL scripts, so a job added from PHP can be processed by a Python worker. Delivery is at least once: a worker that crashes or loses a job's lock can cause that job to run again, which is why the docs tell you to make processors idempotent.

RabbitMQ is a standalone broker. Its strength is routing: exchanges, bindings and queue types decide where a message goes, and consumers acknowledge each message when they finish. It knows nothing about "jobs", so job-level features like progress, results, parent and child jobs or a dashboard of failed jobs are yours to build. Here is how the two compare on the features that matter for background work:

FeatureBullMQ 6RabbitMQ 4.4
What it isLibrary inside your appBroker you run as a separate server
StorageRedis (or Valkey, Dragonfly) or PostgreSQL 13+Its own queues; quorum queues replicate with Raft
LanguagesOfficial SDKs for six languagesAny AMQP 0-9-1 or AMQP 1.0 client
Delayed deliverydelay option on every jobx-opt-delivery-time on quorum queues
Cron and recurring jobsJob Schedulers with cron patterns and time zonesNone built in; needs an external scheduler
Retriesattempts with fixed or exponential backoffDelivery limit, dead lettering, delayed retry with linear backoff
Rate limitingGlobal limiter per queue, plus manual limit on 429Prefetch caps unacked messages, not requests per second
Routing and fan-outQueue per job typeExchanges and bindings, its main strength
02

Which one handles delayed and scheduled jobs better?

BullMQ treats time as a first-class feature. Any job can take a delay in milliseconds, which puts it in a delayed set until the time passes. To run at a specific moment you pass the difference between that moment and now. The delayed jobs guide is honest that the exact second is not guaranteed, since it depends on how busy workers are, but says the timing is accurate in practice. For recurring work, Job Schedulers created with upsertJobScheduler accept an interval or a cron pattern (with an optional seconds field) and a time zone. Upsert means redeploying the same code does not create a duplicate scheduler. Version 6 removed the legacy repeatable-jobs API, so code that still passes repeat to queue.add must move to schedulers before upgrading.

RabbitMQ has had a harder time with delays. For years the answer was the delayed message exchange plugin, and its README now says Team RabbitMQ no longer maintains it. The plugin kept each delayed message on a single node, so losing that node lost the messages, and it was meant for delays of seconds to hours, "a day or two at most". It was also not designed for hundreds of thousands of delayed messages. The replacement arrived in 4.4: according to the quorum queues documentation, a publisher sets x-opt-delivery-time to a future Unix timestamp in milliseconds (a header of type long in AMQP 0-9-1). The message is routed and replicated right away and stays invisible to consumers until that time. No plugin or queue argument is needed, and delayed messages count toward queue length limits. The older pattern, a message TTL that dead-letters into the real queue, still works and is what you use before 4.4.

What RabbitMQ still lacks is a cron. Nothing in the broker publishes a message every weekday at 9am, so a recurring schedule needs a separate process that wakes up and publishes. With BullMQ, delay and cron live in the same library as the worker.

BullMQ: a delayed job, a cron scheduler and a rate-limited workerPostGate API
import { Queue, Worker } from 'bullmq';

const connection = { host: 'localhost', port: 6379 };
const queue = new Queue('publish', { connection });

// One-off job: run at a fixed time. BullMQ takes a delay in ms from now.
const runAt = new Date('2026-11-20T14:00:00Z');
await queue.add(
  'publish-post',
  { postId: 'post_123' },
  {
    delay: runAt.getTime() - Date.now(),
    jobId: 'post_123-20261120T1400Z', // a second add with this id is ignored
    attempts: 5,
    backoff: { type: 'exponential', delay: 2000 },
  },
);

// Recurring job: weekdays at 09:00 in New York (seconds field first)
await queue.upsertJobScheduler(
  'weekday-digest',
  { pattern: '0 0 9 * * 1-5', tz: 'America/New_York' },
  { name: 'publish-digest', data: { feed: 'weekday' } },
);

// Worker: at most 10 jobs per second across every worker on this queue
new Worker('publish', async (job) => publish(job.data), {
  connection,
  limiter: { max: 10, duration: 1000 },
});
03

How do retries and failed jobs compare?

In BullMQ a job fails when the processor throws or when the job stalls more times than allowed. With attempts above 1 it is retried, using a built-in fixed or exponential backoff (with optional jitter) or a custom function. Without a backoff, retries happen immediately. Jobs that run out of attempts land in a failed set you can inspect and retry, which is what most BullMQ dashboards show.

RabbitMQ works at the message level. A consumer that rejects a message or crashes before acknowledging it gets the message redelivered, and quorum queues count failed deliveries in an x-delivery-count header. Past the delivery limit the message is dropped, or dead-lettered if the queue has a dead letter exchange. Since 4.3, quorum queues also support delayed retry with a linear backoff (delay grows with the delivery count up to a maximum). That is usable, but exponential backoff, per-job attempt counts and a browsable list of failed jobs are less work in BullMQ. For the retry policy itself, our post on exponential backoff for social API retries explains which errors are worth retrying at all.

04

Which one can respect an external API rate limit?

This matters as soon as workers call a third-party API. BullMQ's worker option limiter: { max, duration } is global, so 10 workers set to 10 jobs per second still process 10 jobs per second in total. When the API answers 429, a worker can call worker.rateLimit(ms) and throw Worker.RateLimitError() to pause the queue without counting it as a failure. Rate-limited jobs stay in the waiting state.

RabbitMQ's prefetch setting caps how many unacknowledged messages each consumer holds at once. That limits concurrency, which helps, but it is not a requests-per-second limit, so pacing against a quota is logic you write in the consumer.

05

Does BullMQ still need Redis?

No. BullMQ 6 runs the same Queue, Worker, QueueEvents and FlowProducer API on PostgreSQL when you pass createPostgresBackend to the constructor. The PostgreSQL backend guide requires PostgreSQL 13 or newer (14+ recommended) and the pg package, and it notes that Redis remains the default and most battle-tested option. Choose Postgres when you would rather not run Redis or want jobs in the same database as your data.

If you stay on Redis, the production guide has two settings people miss. Set maxmemory-policy to noeviction, because a Redis that evicts keys like a cache will silently drop queue data, and turn on AOF persistence, which many hosted Redis plans leave off. pg-boss is the other common Postgres-only option for Node: it claims jobs with SKIP LOCKED and supports cron schedules and deferred jobs.

06

Where do Kafka and SQS fit in this comparison?

Neither is a job scheduler. Kafka is a distributed log for event streams that many consumers read at their own pace, which makes it a poor fit for "run this one task at 2pm on the 20th". Amazon SQS can hide new messages for a while, but its delay queues top out at 15 minutes. With either one, anything due later than that needs a database row and a process that enqueues it when the time comes.

07

Which should you use for scheduled social media posts?

Scheduled posts are a good test case because they combine everything above: a fixed publish time days or weeks away, recurring slots, retries against APIs that fail in odd ways, and per-platform rate limits. Use this checklist to decide:

  1. Single Node or Python app, jobs owned by the app: BullMQ. Delays, cron, backoff and the rate limiter cover the whole job.
  2. Several services in different languages reacting to the same event: RabbitMQ for the routing, and keep scheduling in a database or a job library.
  3. Posts scheduled weeks ahead: keep the post in your database as the source of truth and only enqueue jobs that are due soon. A job sitting in a queue for three weeks is hard to edit, cancel or move when the user changes the time.
  4. Already on Postgres and no Redis: BullMQ's Postgres backend or pg-boss, and you can skip a new piece of infrastructure.
  5. Every case: give each job a deterministic id. BullMQ ignores a second job with an existing jobId, but only while the first one is still stored, so a job removed on completion does not block a duplicate.

The queue is also the smaller half of the problem. Each network has its own OAuth refresh, media processing and error codes, which is the part our breakdown of how social media schedulers work goes through. If publishing is a feature of your product rather than the product itself, PostGate's schedules API takes a cron expression or daily slots with an IANA time zone, and the delivery lifecycle reports status for each platform target, so your own queue only has to call one API.

Frequently asked questions

Is BullMQ better than RabbitMQ?

For background jobs inside one application, usually yes, because delays, cron schedules, retries with backoff and rate limiting are built in. For routing messages between many services written in different languages, RabbitMQ is the better tool. They also work together: RabbitMQ carries events and BullMQ runs the jobs they trigger.

Does RabbitMQ support delayed messages without a plugin?

Yes, since RabbitMQ 4.4. Quorum queues accept an x-opt-delivery-time header with a future Unix timestamp in milliseconds and hold the message until then. The old delayed message exchange plugin is no longer maintained by the RabbitMQ team.

Can BullMQ run without Redis?

Yes. BullMQ 6, released in July 2026, added a PostgreSQL backend that needs PostgreSQL 13 or newer and the pg package. Redis, Valkey and Dragonfly are still supported, and Redis remains the default.

How do I run a cron job with BullMQ?

Call queue.upsertJobScheduler with an id, a repeat option such as { pattern: '0 0 9 * * 1-5', tz: 'America/New_York' } and a job template. Upserting with the same id updates the scheduler instead of creating a duplicate. The old repeat option on queue.add was removed in version 6.

Build it with PostGate

One API for every social platform your customers care about — sandboxed, documented, and ready today.

Start building Read the docs