SYSTEM DESIGN · MESSAGING · ~13 MIN READ

Queues and Messaging

Every back pressure queue needs an engine underneath. Redis, RabbitMQ, Kafka, Celery, and BullMQ solve the same underlying problem — holding work and decoupling systems —, each in its own way.

Diagram showing the flow of user, API, queue, worker and database, with RabbitMQ, Redis, BullMQ and Celery listed as the pieces behind the queue.
QUEUE + WORKER + BACK PRESSURE · THE PIECES BEHIND THE ASYNC FLOW

SECTION 01

Why queues and workers exist

In the back pressure article, the queue shows up as an abstract piece: a bounded space between the API and the workers that keeps input from destroying a slower component downstream. But "queue" isn't one thing. In practice it's implemented by different tools, and each one solves a different part of the problem.

ROLESGENERIC
Producer
   ↓
Broker / Queue
   ↓
Consumer / Worker

Redis stores fast in-memory data structures. RabbitMQ routes messages with delivery guarantees. Kafka keeps an event log that multiple independent consumers can read. Celery and BullMQ are libraries that run background tasks using one of these brokers underneath.

This article walks through each of these pieces and ends by showing exactly where back pressure fits in when they work together.

SECTION 02

Redis: the multipurpose piece

Redis is an in-memory data structure store. The simplest way to think about it: a giant, very fast dictionary that also knows how to store lists, sets, hashes, and streams — not just key-value pairs.

Illustrated poster of Redis explaining it is an in-memory data structure used as cache, queue and pub/sub, with use cases like cache-aside, sessions, rate limiting and distributed locks.
REDIS · IN-MEMORY DATA STORE

Because of that versatility, Redis becomes the base for several different things: cache (see cache strategies), user sessions, rate limiting, distributed locks, and, with a list or a Stream, a simple queue.

SIMPLE QUEUEREDIS
LPUSH queue:emails "{ \"to\": \"a@x.com\" }"

BRPOP queue:emails 0
REDIS ALONE IS NOT A FULL BROKER

A Redis list has no ACK, no automatic retry, and no dead-letter queue: if the worker crashes after BRPOP and before finishing the job, the message is gone. That's why tools like BullMQ exist — they use Redis as storage but add the guarantee layer on top.

SECTION 03

RabbitMQ: message broker

RabbitMQ is a dedicated message broker: a server whose only job is to receive, route, and deliver messages between producers and consumers, with delivery confirmation.

Illustrated poster of RabbitMQ showing the flow producer, exchange, queue and consumer, with use cases like async jobs, notifications and dead-letter queue.
RABBITMQ · MESSAGE BROKER
FLOWRABBITMQ
Producer
   ↓
Exchange (routes by rule)
   ↓
Queue
   ↓
Consumer → ACK

The extra piece compared to a Redis queue is the explicit ACK: the consumer only confirms the message after finishing processing it. If the consumer crashes mid-job, RabbitMQ redelivers the message to another worker — nothing is lost because of an isolated crash.

GOING DEEPER: EXCHANGE

The exchange decides which queue each message goes to. A direct exchange routes by an exact key; a topic exchange routes by pattern (order.*); a fanout exchange copies the message to every queue bound to it. Messages that keep failing can land in a dead-letter queue instead of retrying forever.

SECTION 04

Kafka: event streaming

Kafka solves a different problem than RabbitMQ. Instead of a queue that empties as it's consumed, Kafka keeps an event log: every message published to a topic stays retained for a configurable time, and any consumer can read it — more than once, if needed.

Illustrated poster of Kafka showing a producer publishing to a topic with partitions, read by multiple consumers, with use cases like domain events, analytics and replay.
KAFKA · EVENT STREAMING
FLOWKAFKA
Producer
   ↓
Topic (partitions)
   ↓
Consumer A · Consumer B · Consumer C
   (each reads independently)

That changes what's possible: a payment.completed event can be read at the same time by an analytics service, an audit service, and a recommendation service, with none of them depending on each other or "stealing" the message from the rest.

QUEUE VS LOG

In RabbitMQ, the message disappears from the queue after ACK. In Kafka, the event stays in the log until the retention window expires — which enables replay: reprocessing the last N events, or spinning up a new consumer that reads everything from the start.

SECTION 05

Celery: Python task queue

Celery is a Python framework for running background tasks with workers. It isn't a broker itself — it needs one (usually Redis or RabbitMQ) to store pending tasks.

Illustrated poster of Celery showing the flow Python app, broker Redis or RabbitMQ and Celery worker, with use cases like emails, ETL and periodic tasks.
CELERY · DISTRIBUTED TASK QUEUE
EXAMPLEPYTHON
@app.task(bind=True, max_retries=3)
def send_email(self, user_id):
    try:
        email_service.send(user_id)
    except Exception as exc:
        raise self.retry(exc=exc, countdown=10)

# in the API:
send_email.delay(user_id)

The .delay(...) call publishes the task to the broker and returns immediately. A Celery worker, running separately from the API, picks up the task and executes it. Retries, periodic scheduling (Celery beat), and long-running jobs are already handled by the framework.

SECTION 06

BullMQ: Node.js task queue

BullMQ plays the same role as Celery, but for the Node/TypeScript ecosystem, and it's built directly on top of Redis.

Illustrated poster of BullMQ showing the flow Node API, Redis, queue and worker, with use cases like notifications, team generation and async processing.
BULLMQ · NODE.JS JOB QUEUE
EXAMPLETYPESCRIPT
const queue = new Queue("notifications", { connection: redis });
await queue.add("push", { userId }, { attempts: 3 });

new Worker("notifications", async (job) => {
  await pushService.send(job.data.userId);
}, { connection: redis, concurrency: 20 });

That concurrency setting isn't a minor detail — it's exactly the kind of limit discussed in back pressure: how many jobs this worker processes at once. If 5000 notifications arrive at once, the queue grows, but the worker keeps consuming at the pace it can handle, instead of trying to run everything at the same time.

SECTION 07

Task vs event: command or fact?

With all five tools on the table, it's easier to see a distinction that trips up a lot of people: task and event are not synonyms.

Comparison between a task, a work instruction like send_email, and an event, a fact that happened like payment.completed, with the practical rule of using a job queue for tasks and a stream for events.
TASK VS EVENT · COMMAND VS EVENT

A task is a work instruction — "do this": send_email, generate_teams, resize_image. Usually a single worker executes it, and the result matters to whoever requested it. An event is a fact that already happened — "this happened": payment.completed, match.cancelled. Multiple consumers can react to it independently, and no "owner" needs to know who's listening.

PRACTICAL RULE

Task → job queue (BullMQ, Celery, RabbitMQ). Event → stream / event bus (Kafka). Mixing the two is common early on: using Kafka to say "generate this PDF," or using a job queue to tell ten different services that a payment was confirmed. Both work poorly when used in the other's role.

SECTION 08

Putting it together: a mini e-commerce

Placing the pieces side by side in one system makes it clearer where each one fits.

Diagram of an e-commerce system showing a Node API querying PostgreSQL and Redis synchronously, publishing jobs to BullMQ and Redis for notification, billing and PDF workers, and events on Kafka for analytics, audit and recommendation consumers.
MINI DS · E-COMMERCE · HOW IT ALL FITS TOGETHER

Creating an order is synchronous: the API queries PostgreSQL, confirms stock, and responds — the same consistency path described in back pressure. Notifying the customer, generating the PDF invoice, and refreshing the cache are async: they become jobs in a BullMQ/Redis queue, processed by dedicated workers. Analytics, auditing, and recommendations don't owe anyone an immediate response: they're independent consumers of an event on Kafka.

ToolTypeWhen to use
RedisCache / fast data structureFast reads, sessions, rate limiting, locks
RabbitMQMessage brokerDecoupling services with delivery guarantees and ACK
KafkaEvent streamingMultiple consumers, high throughput, replay
CeleryTask queue (Python)Background jobs in the Python ecosystem
BullMQTask queue (Node)Background jobs in the Node/TS ecosystem

SECTION 09

Core idea

None of these tools replace the discipline of back pressure — they just supply the material it's built from.

LAYERSWITH THE PIECES
Internet
   ↓
Rate Limit
   ↓
Concurrency Limit
   ↓
Connection Pool
   ↓
Bounded Queue (RabbitMQ / BullMQ / Celery)
   ↓
Workers
   ↓
PostgreSQL · Kafka (events) · Redis (cache)

Swapping Redis for RabbitMQ, or RabbitMQ for Kafka, doesn't remove the question raised in back pressure: what happens when more work comes in than the system can drain? Each tool changes the technical answer — queue size, ACK, retention, replay — but the architectural decision stays the same: accept only as much as you can handle well, and make clear what happens to the excess.

END · THANKS FOR READING

The queue is just the interface. What matters is the contract behind it.

Redis, RabbitMQ, Kafka, Celery, and BullMQ solve different parts of the same problem: how to accept work without taking down whatever comes next.

READ BACK PRESSURE BACK TO CONTENTS