RabbitMQ

Intro

RabbitMQ is an open-source message broker implementing AMQP 0-9-1, where producers publish to exchanges and messages are routed to queues through bindings and routing keys before consumers process them. It matters because it decouples producers and consumers, enables asynchronous processing, and absorbs traffic spikes without forcing synchronous dependency chains. In interviews, reach for RabbitMQ when you need task queues, request-reply, pub/sub fan-out, or fair work distribution across multiple workers. In a Webhook -> Queue -> Worker system, RabbitMQ is usually the safety valve between bursty ingress and bounded worker throughput.

AMQP Model

RabbitMQ routing is explicit: producers publish to an exchange, not directly to a queue (except via the default exchange behavior).

flowchart LR
    P[Producer] --> E[Exchange]
    E -->|binding with routing key| Q[Queue]
    Q --> C[Consumer]

Core parts:

Exchange Types

Type Rule Typical use
Direct Exact routing key match Command-style task queues (order.created)
Fanout Broadcast to all bound queues One event consumed by many services
Topic Pattern match with * and # Domain events with taxonomy (order.*, payment.#)
Headers Match message headers Complex metadata-based routing

Delivery Guarantees

RabbitMQ provides mechanisms to build guarantees; it does not make exactly-once automatic.

At-most-once

Use this only when occasional loss is acceptable.

At-least-once

Tradeoff: duplicates are possible; consumers must be idempotent.

Exactly-once

RabbitMQ does not natively offer end-to-end exactly-once delivery. Achieve business-level exactly-once behavior with idempotent consumers, deduplication keys, and producer outbox patterns.

Confirms, Ack, Nack, Reject, DLX

C# Example (RabbitMQ.Client)

Producer

using System.Text.Json;
using RabbitMQ.Client;

var factory = new ConnectionFactory { HostName = "localhost" };

await using var connection = await factory.CreateConnectionAsync();
await using var channel = await connection.CreateChannelAsync(
    new CreateChannelOptions(
        publisherConfirmationsEnabled: true,
        publisherConfirmationTrackingEnabled: true));

await channel.QueueDeclareAsync(
    queue: "orders",
    durable: true,
    exclusive: false,
    autoDelete: false,
    arguments: null);

var order = new Order("ord-1001", "cust-42", 129.50m);
var body = JsonSerializer.SerializeToUtf8Bytes(order);

var props = new BasicProperties
{
    DeliveryMode = 2,
    MessageId = order.OrderId,
    ContentType = "application/json"
};

await channel.BasicPublishAsync(
    exchange: "",
    routingKey: "orders",
    mandatory: true,
    basicProperties: props,
    body: body);

public sealed record Order(string OrderId, string CustomerId, decimal Amount);

Consumer

using System.Text.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

var factory = new ConnectionFactory { HostName = "localhost" };

await using var connection = await factory.CreateConnectionAsync();
await using var channel = await connection.CreateChannelAsync();

await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 32, global: false);

var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (_, ea) =>
{
    try
    {
        var order = JsonSerializer.Deserialize<Order>(ea.Body.Span);
        if (order is null)
        {
            await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: false);
            return;
        }

        await ProcessOrderAsync(order);
        await channel.BasicAckAsync(ea.DeliveryTag, multiple: false);
    }
    catch (TransientDependencyException)
    {
        await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: true);
    }
    catch
    {
        await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: false);
    }
};

await channel.BasicConsumeAsync(queue: "orders", autoAck: false, consumer: consumer);

await Task.Delay(Timeout.InfiniteTimeSpan);

static Task ProcessOrderAsync(Order order) => Task.CompletedTask;

public sealed class TransientDependencyException : Exception;
public sealed record Order(string OrderId, string CustomerId, decimal Amount);

Key Operational Concepts

Prefetch count (QoS)

Message TTL

Queue length limits

Lazy queues

Quorum queues

RabbitMQ vs Kafka

Dimension RabbitMQ Kafka
Model Queue broker with exchanges and bindings Partitioned append-only log
Ordering Per queue/consumer semantics; strict global ordering is hard Strong ordering within partition
Replay Not a core design goal Native replay by offset
Consumer groups Competing consumers per queue Native group coordination and offset commits
Throughput Strong low-latency messaging, typically lower peak throughput than Kafka Very high throughput streaming
Routing flexibility Rich routing (direct/fanout/topic/headers) Simpler topic/partition routing
Operational complexity Easier onboarding, careful tuning still required Heavier ops footprint but strong stream ecosystem

Pitfalls

1) Unbounded queues without TTL and length limits

2) Auto-ack in production

3) Relying on classic mirrored queues

4) Not setting prefetch

Interview Questions

How do you use RabbitMQ to absorb bursty ingress when producers outpace consumers?

Expected answer:

Why: demonstrates backpressure design, reliability, and operational thinking.

How do you implement at-least-once delivery in RabbitMQ, and what new risk appears?

Expected answer:

Why: shows understanding of guarantees and their costs.

References


Whats next