SGShubham Goyal
HomeProjectsBlogContactAvailableHire me ↗

Full-stack developer building scalable systems and clean user experiences.

linkedingithubdiscordtwitterinstagramtelegram

Navigate

HomeProjectsBlogContact

Resources

RésuméGitHubLinkedIn

Get in touch

[email protected]Hire me
© 2026 SHUBHAM GOYAL · ALL RIGHTS RESERVEDPrivacy·Terms· BUILT IN INDIA · v2.0
Blog

Backend

Queues and Message Brokers: Redis, Kafka, RabbitMQ and How to Choose

A practical guide to queues and message brokers: how message delivery works, how Redis, Kafka, and RabbitMQ differ, where each fits, and how I think about choosing between them.

Shubham Goyal

7th October 2026

1
Queues and Message Brokers: Redis, Kafka, RabbitMQ and How to Choose

When building backend systems, I often need to move work from one service to another without making the caller wait for the entire operation. Sending an email, processing an image, running a scraper, generating a report, processing payments, or consuming events can all be handled asynchronously.

That is where queues and message brokers come in. Redis, RabbitMQ, and Kafka can all move data between processes, but they are designed around different models. Understanding those differences is more useful than memorizing which tool is supposed to be the fastest.

What is a queue?

At the simplest level, a queue stores messages until a consumer is ready to process them. A producer creates a message, the queue holds it, and a consumer reads and processes it.

text
Producer                    Queue                    Consumer
   |                           |                         |
   | ---- message -----------> |                         |
   |                           |                         |
   |                           | ---- message ---------> |
   |                           |                         |
   |                           | <---- acknowledgement - |

The important part is that the producer and consumer do not have to operate at exactly the same time or speed. The queue provides a buffer between them.

Why use a queue?

Without a queue, a service commonly has to call another service synchronously and wait for the response. That creates tighter coupling and means temporary slowdowns in one component can directly affect another.

  • Decouple producers from consumers.
  • Move slow or expensive work outside the request path.
  • Absorb temporary traffic spikes.
  • Allow multiple workers to process work concurrently.
  • Retry failed work instead of losing it immediately.
  • Create asynchronous workflows between services.
  • A basic asynchronous architecture

    text
    +----------------+
                      |   API Server    |
                      +-------+--------+
                              |
                              | publish job
                              v
                      +----------------+
                      | Queue / Broker |
                      +-------+--------+
                              |
                 +------------+------------+
                 |            |            |
                 v            v            v
            +---------+  +---------+  +---------+
            | Worker 1|  | Worker 2|  | Worker 3|
            +---------+  +---------+  +---------+

    For example, an API can accept an image upload and immediately create a processing job. Workers can consume those jobs independently. The API does not need to keep the HTTP request open while the image is processed.

    Queue semantics matter

    The word queue alone does not describe how messages behave. When choosing a system, I look at delivery semantics, ordering, persistence, retries, acknowledgements, retention, consumer behavior, and scaling.

    Acknowledgements

    A consumer may need to explicitly acknowledge successful processing. If the worker crashes before acknowledging the message, the system can make the message available again depending on the broker and its configuration.

    text
    Message available
           |
           v
       Consumer gets message
           |
           +------ processing succeeds ------> ACK
           |
           +------ consumer crashes ---------> message can be redelivered

    At-most-once and at-least-once delivery

    At-most-once delivery means a message is delivered zero or one time. A message can be lost, but duplicate processing is avoided by the delivery model.

    At-least-once delivery means a message is delivered one or more times. This is useful when losing work is worse than processing it twice, but consumers need to handle duplicates safely.

    In practice, I design consumers with idempotency in mind. If processing the same message twice can create two payments, two orders, or two side effects, the consumer needs a strategy to detect or safely handle duplicates.

    Ordering

    Some workloads require messages to be processed in order. Others only care that every message is eventually processed. Ordering requirements can affect partitioning, concurrency, and the choice of messaging system.

    Queue versus event stream

    One of the most important distinctions is whether I want work to be consumed from a queue or events to remain available as a stream that consumers can read independently.

    text
    Traditional work queue
    
    Producer ---> [ A ][ B ][ C ][ D ] ---> Worker
                                          
    Messages are generally treated as work waiting to be consumed.
    
    
    Event stream
    
    Producer ---> [ A ][ B ][ C ][ D ][ E ] ---> Consumer A
                                          |
                                          +-------> Consumer B
                                          |
                                          +-------> Consumer C
    
    Multiple consumers can maintain their own position in the stream.

    This distinction is one reason Kafka is often discussed differently from traditional message queues. Kafka is built around durable append-only logs and consumer positions, while systems such as RabbitMQ are commonly used for message routing and work distribution.

    Redis as a queue

    Redis is primarily an in-memory data store, but it provides several primitives that can be used for asynchronous processing. Depending on the problem, Redis lists, Pub/Sub, Streams, or other structures can be used.

    Redis Lists

    A simple list can represent a basic work queue. A producer pushes an item and a worker blocks waiting for an item.

    text
    Producer
       |
       | LPUSH
       v
    +-------------------+
    | job3 | job2 | job1|
    +-------------------+
              ^
              |
            BRPOP
              |
           Worker

    This is straightforward and useful for relatively simple background jobs. However, a production queue often needs more than push and pop operations, such as acknowledgements, consumer groups, replay, retention, and stronger delivery controls.

    Redis Streams

    Redis Streams provide a log-like data structure with message IDs, consumer groups, pending entries, and acknowledgement mechanisms. They are more appropriate than a basic Redis list when I need richer stream-processing behavior while keeping Redis in the architecture.

    text
    XADD stream * type email user_id 123
    
                 Redis Stream
    +------------------------------------------------+
    | 171...-0 | 171...-1 | 171...-2 | 171...-3     |
    +------------------------------------------------+
           |          |          |          |
           +----------+----------+----------+
                        |
                  Consumer Group
                 +------+------+
                 |             |
              Worker A      Worker B

    Redis Streams can be a good fit when I already depend on Redis and need queue or stream functionality without introducing another infrastructure component.

    Redis Pub/Sub is different

    Redis Pub/Sub should not be treated as the same thing as a durable queue. A publisher sends a message to a channel and currently connected subscribers receive it. It is useful for transient notifications, but it is not the same model as a durable work queue.

    text
    Publisher
        |
        v
    +-----------+
    |  Channel  |
    +-----+-----+
          |
      +---+---+
      |       |
      v       v
    Sub A   Sub B
    
    If a subscriber is not connected when the message is published,
    the Pub/Sub model does not provide the same durable replay behavior
    as a persistent stream.

    Kafka

    Kafka is designed around durable distributed logs. Producers append records to topics, topics are divided into partitions, and consumers track their position in those partitions.

    text
    Kafka Topic
    +------------------------------------------------------+
    | Partition 0 | A | B | C | D | E | F | G |           |
    +------------------------------------------------------+
    | Partition 1 | H | I | J | K | L | M | N |           |
    +------------------------------------------------------+
    | Partition 2 | O | P | Q | R | S | T | U |           |
    +------------------------------------------------------+
            ^                         ^
            |                         |
       Consumer A                Consumer B
       offset = 4                offset = 6

    The important idea is that consuming a Kafka record does not mean the record immediately disappears from the topic. Records are retained according to the configured retention policy, and consumers track their offsets.

    Kafka partitions

    Partitions provide a way to distribute a topic across brokers and consumers. A consumer group can divide partitions among its consumers so that multiple workers process records concurrently.

    text
    Topic: orders
    
    Partition 0 ---> Consumer 1
    Partition 1 ---> Consumer 2
    Partition 2 ---> Consumer 3
    Partition 3 ---> Consumer 4

    Within a partition, records have an ordered sequence. If I need ordering for related events, I can design the partitioning strategy so those events are placed in the same partition.

    RabbitMQ

    RabbitMQ is a message broker focused heavily on message routing, queues, acknowledgements, and delivery semantics. Producers publish messages to exchanges, and exchanges route messages to queues.

    text
    Producer
       |
       v
    +----------+
    | Exchange |
    +----+-----+
         |
         | routing
      +--+--------+---------+
      |          |         |
      v          v         v
    Queue A    Queue B   Queue C
      |          |         |
      v          v         v
    Worker A   Worker B  Worker C

    The exchange model is useful when I need flexible routing. Messages can be routed based on routing keys, patterns, or other exchange behavior instead of having producers directly choose a particular worker queue.

    RabbitMQ acknowledgements

    A consumer can acknowledge a message after successful processing. If the consumer fails before acknowledging it, RabbitMQ can requeue the message depending on the configuration and failure mode.

    Redis versus RabbitMQ versus Kafka

    These technologies overlap, but I would not consider them interchangeable in every architecture.

  • Redis is useful when I need simple queues, streams, caching, low-latency data structures, or I already have Redis as part of the system.
  • RabbitMQ is useful when message routing, acknowledgements, work queues, retries, and broker-managed delivery behavior are central requirements.
  • Kafka is useful when I need durable event streams, high-throughput distributed processing, partitions, consumer groups, and the ability for consumers to track and replay retained events.
  • A practical comparison

    text
    Redis        RabbitMQ        Kafka
    
    Primary model             Data store   Message broker  Distributed log
    
    Simple work queues        Yes          Yes             Possible
    Message routing           Basic        Strong          Topic based
    Consumer groups           Streams      Consumers       Core concept
    Durable event history     Streams      Queue based     Core concept
    Replay retained events    Streams      Limited model   Core concept
    Partitioned log           No           No              Yes
    Very simple setup         Yes          Moderate        More involved
    Caching + queue together  Yes          No              No

    Where I would use Redis

    For a small or medium-sized application, Redis can be a practical choice when the application already uses Redis for caching, rate limiting, sessions, or other data structures. Adding a simple queue or Redis Stream can avoid introducing another infrastructure component.

  • Background jobs.
  • Simple worker queues.
  • Short-lived asynchronous tasks.
  • Real-time applications where Redis is already part of the architecture.
  • Stream processing using Redis Streams.
  • Where I would use RabbitMQ

    I would consider RabbitMQ when the application is fundamentally about delivering jobs or messages to consumers and I need explicit routing and acknowledgement behavior.

  • Task queues.
  • Background processing.
  • Microservice communication.
  • Applications with multiple routing patterns.
  • Workloads where explicit message acknowledgement and broker-managed delivery are important.
  • Where I would use Kafka

    I would consider Kafka when the problem looks more like event streaming than a simple job queue. The ability to retain records and let consumers independently track their positions changes how I can build the system.

  • Event-driven architectures.
  • High-throughput event processing.
  • Analytics pipelines.
  • Multiple independent consumers that need the same event history.
  • Systems where consumers need to track offsets and process retained events.
  • A common mistake: choosing Kafka for every queue

    Kafka is powerful, but that does not automatically make it the right choice for every background job. If my requirement is simply to put email jobs into a queue and have workers process them, a simpler queueing system may be easier to operate and reason about.

    I try to start with the messaging semantics I need rather than the technology I already know. If I need a durable event log with independent consumers, Kafka becomes interesting. If I need broker-based work distribution and routing, RabbitMQ may fit better. If I already run Redis and need straightforward queue or stream capabilities, Redis may be enough.

    Retries and failed messages

    A queue becomes much more useful when failure is treated as a normal part of distributed systems. Network failures, temporary database errors, service crashes, malformed messages, and downstream outages can all cause processing failures.

    text
    +----------------+
                 |    Queue       |
                 +-------+--------+
                         |
                         v
                    +---------+
                    | Worker  |
                    +----+----+
                         |
                  +------+------+
                  |             |
               success        failure
                  |             |
                  v             v
                 ACK       retry / retry queue
                                |
                                v
                           dead-letter queue

    For retries, I avoid blindly retrying forever. A practical design can use a retry limit, backoff, and a dead-letter or failure-handling path for messages that repeatedly fail.

    Idempotent consumers

    At-least-once processing means duplicate delivery is possible. Because of that, I prefer consumers that are safe to execute more than once.

    go
    func processOrder(ctx context.Context, orderID string) error {
        processed, err := store.AlreadyProcessed(ctx, orderID)
        if err != nil {
            return err
        }
    
        if processed {
            return nil
        }
    
        if err := createOrderSideEffects(ctx, orderID); err != nil {
            return err
        }
    
        return store.MarkProcessed(ctx, orderID)
    }

    The exact implementation depends on the application. The important principle is that message delivery and business-side effects should be designed together.

    Backpressure

    Queues also provide a way to absorb differences between production and consumption rates. If producers create work faster than workers can process it, the queue grows.

    text
    Producer rate > Consumer rate
    
    Producer ---> ---> ---> --->
                      |
                      v
                 +----------+
                 |  QUEUE   |
                 |  growing |
                 +----------+
                      |
                      v
                   Worker

    A growing queue is not automatically a failure. It becomes a capacity problem when the backlog grows continuously or when message age exceeds the application's acceptable processing delay.

    Scaling workers

    A common way to process more work is to add workers. The queue or broker distributes work according to its delivery model.

    text
    Queue
                        |
              +---------+---------+
              |         |         |
              v         v         v
           Worker 1  Worker 2  Worker 3
              |         |         |
              +---------+---------+
                        |
                   More throughput

    This works only when the downstream resources can also handle the increased concurrency. Adding workers does not help if the database, external API, CPU, or another bottleneck becomes saturated.

    Queue versus synchronous HTTP

    text
    Synchronous
    
    Client ---> API ---> Service ---> Database
      |                                  |
      +----------- waits ---------------+
    
    
    Asynchronous
    
    Client ---> API ---> Queue ---> Worker ---> Database
      |
      +------ response returned earlier

    I use asynchronous processing when the client does not need the final result immediately. If the operation must complete before the response can be returned, introducing a queue may add complexity without solving the actual problem.

    A simple Go worker

    The basic worker pattern is independent of the specific broker. The worker continuously receives work, processes it, and acknowledges successful completion.

    go
    func worker(ctx context.Context, jobs <-chan Job) {
        for {
            select {
            case <-ctx.Done():
                return
            case job := <-jobs:
                if err := process(ctx, job); err != nil {
                    // retry or move to failure handling
                    continue
                }
            }
        }
    }

    In a real implementation, the jobs channel would be connected to Redis, RabbitMQ, Kafka, or another messaging system, and the failure path would need explicit retry and acknowledgement behavior.

    How I decide which one to use

  • Define whether I need a work queue or an event stream.
  • Determine whether messages must survive process or service failures.
  • Define the ordering requirements.
  • Decide whether messages need to be replayed by consumers.
  • Define acknowledgement and retry behavior.
  • Estimate throughput, concurrency, and backlog requirements.
  • Check whether an existing infrastructure component already solves most of the problem.
  • Only then choose the messaging technology.
  • The mental model I use

    I think about Redis, RabbitMQ, and Kafka as tools with overlapping capabilities but different primary abstractions. Redis gives me powerful data structures and can provide queues and streams. RabbitMQ gives me a broker-oriented model centered around routing and message delivery. Kafka gives me a distributed event log with partitions, retention, and consumer offsets.

    The right choice depends less on which system has the highest theoretical throughput and more on the semantics the application needs. A simple queue should remain simple. An event platform should provide durable streams and independent consumers. The messaging model should follow the problem.

    Responses

    0 comments

    No responses yet. Be the first to share your thoughts.

    Leave a response

    Tags

    #queues#message-queues#redis#kafka#rabbitmq#event-driven#distributed-systems#backend#go#microservices

    Want to work together?

    Get in touch ↗
    All posts