IAnswerable

Choosing a Message Queue

15 Jul 2026

The message queue you should use is almost always the one closest to what you already run. Adding a new piece of infrastructure for queueing is rarely the constraint — picking the wrong shape of guarantee for your workload is.

Start here: do you already run Postgres or Redis?

If yes, you very likely don’t need a dedicated queue product yet.

Postgres as a queue works well when jobs are relatively low-volume (up to a few hundred per second) and you want the job’s state to live next to the data it’s operating on, in the same transaction:

SELECT * FROM jobs
WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;

SKIP LOCKED is the trick that makes this safe with multiple workers — each worker grabs different unclaimed rows instead of blocking on each other. You get transactional job creation (enqueue a job in the same transaction as the business logic that created it — no separate system to keep in sync), visibility via normal SQL, and one less thing to operate.

Redis (via a library like BullMQ or Sidekiq) works well when you need higher throughput than Postgres comfortably handles, or you want built-in retry/backoff/rate-limiting without writing it yourself. You give up the transactional guarantee — a job can get enqueued in Redis even if the surrounding Postgres transaction later rolls back, unless you’re careful to enqueue only after commit.

When to actually reach for a dedicated system

Need Reach for
Already on Postgres, moderate volume, want jobs transactional with your data Postgres + SKIP LOCKED
Need retries, delays, rate limiting, already run Redis Redis (BullMQ, Sidekiq)
Fully managed, pay-per-use, low ops burden, AWS-native SQS
Multiple independent consumers need the same event, need replay Kafka / a log-based system
Need routing rules between many producers/consumers (topic/exchange patterns) RabbitMQ

SQS is the right default once you’re on AWS and don’t want to operate anything. You lose some flexibility (no complex routing, at-least-once delivery means your consumers must be idempotent) in exchange for essentially zero operational burden.

Kafka earns its cost when multiple independent consumers need to read the same stream of events, potentially replaying from history — think analytics, audit logging, and real-time processing all reading the same “order placed” stream independently. It is real operational weight: partitioning, consumer groups, retention tuning. Don’t reach for it because you have “a queue,” reach for it because you have multiple readers of one durable event log.

RabbitMQ fits when you need genuinely complex routing — different message types fanned out to different consumers based on rules — and you want that routing logic in the broker rather than hand-rolled in application code.

The mistake to avoid

Picking Kafka because it’s the name everyone knows, for a workload that’s one producer, one consumer, a few hundred jobs a minute. That’s Postgres with SKIP LOCKED, running in an afternoon, with no new service to operate, monitor, and eventually debug at 2am.

The one thing every option requires

Whichever you pick, your consumers must handle a message arriving twice. Every one of these systems is at-least-once delivery in practice — network blips and consumer crashes mean redelivery will happen eventually, regardless of vendor promises. Design job handlers to be idempotent (check if the work’s already done before doing it again) from the start, because retrofitting idempotency after a duplicate-charge incident is a much worse afternoon than building it in on day one.