Backend / Message queues / RabbitMQ / 04_rabbitmq_interview.md

RabbitMQ — Common Interview Questions and Answers

Updated 4 interview angles 8 min read source
On this page22
  1. 1. What is RabbitMQ and when do you use it?
  2. 2. Producer → Exchange → Queue → Consumer — explain.
  3. 3. What’s an exchange binding?
  4. 4. Classic vs Quorum vs Stream queues?
  5. 5. How do you make messages durable?
  6. 6. What’s a DLX and when do you use it?
  7. 7. What’s the prefetch count and how do you tune it?
  8. 8. What’s the difference between basic.ack, basic.nack, and basic.reject?
  9. 9. RabbitMQ vs Kafka — when each?
  10. 10. How does RabbitMQ achieve HA?
  11. 11. What’s a publisher confirm?
  12. 12. What’s the mandatory flag?
  13. 13. Why are channels not threads-safe?
  14. 14. What’s a lazy queue?
  15. 15. How do you handle a poison message?
  16. 16. What’s RabbitMQ’s “shovel”?
  17. 17. RabbitMQ vs Celery — what’s the relationship?
  18. 18. Can you do RPC with RabbitMQ?
  19. 19. How does message TTL work?
  20. 20. Production tips?
  21. Mental model
  22. Interview angle

RabbitMQ — Common Interview Questions and Answers

1. What is RabbitMQ and when do you use it?

RabbitMQ is a message broker implementing AMQP 0.9.1 (with extensions). It accepts messages from producers, routes them through exchanges to queues, and delivers to consumers.

Use it for:

  • Work queues (background jobs, async processing).
  • Pub/sub with multiple consumers.
  • RPC-style request/response.
  • Decoupling services with complex routing needs.

It’s not optimized for:

  • Very high throughput streaming (Kafka wins).
  • Long-term log retention with replay (Kafka wins).

2. Producer → Exchange → Queue → Consumer — explain.

Producers don’t send to queues directly; they publish to exchanges with a routing key. The exchange routes to bound queues based on its type:

  • direct — exact routing key match.
  • topic — pattern match (orders.*.placed, #.failed).
  • fanout — broadcast to all bound queues.
  • headers — match by header values.

A queue gets messages only if it’s bound to an exchange with a binding rule that matches the routing key.

3. What’s an exchange binding?

A binding is a routing rule attaching a queue to an exchange.

python
channel.queue_bind(exchange="orders", queue="email", routing_key="order.placed")

The exchange consults bindings on each publish: “for this routing key, which queues should I deliver to?” One queue can be bound to multiple exchanges; one exchange can have many queues bound.

4. Classic vs Quorum vs Stream queues?

Classic Quorum Stream
Replication mirroring (deprecated) Raft-based, strong consistency replicated log
Use case (deprecated) work queues, durable messaging event streaming, fan-out
Reads once per consumer once per consumer replayable by offset
Tooling mature but legacy modern default newer, less mature

For new deployments: quorum queues for tasks, streams for streaming. Avoid classic mirrored queues.

5. How do you make messages durable?

Three things, all required:

  1. Durable queuedurable=True on queue_declare. Queue survives broker restart.
  2. Persistent messagesproperties=BasicProperties(delivery_mode=2). Messages written to disk.
  3. Publisher confirmschannel.confirm_delivery() + check return value. Broker acknowledges receipt.

Even with all three, you have at-least-once delivery, not exactly-once. Consumer must be idempotent.

6. What’s a DLX and when do you use it?

Dead-Letter Exchange — set x-dead-letter-exchange on a queue; rejected/expired/over-limit messages route there.

python
channel.queue_declare(queue="orders", arguments={
    "x-dead-letter-exchange": "dlx",
    "x-dead-letter-routing-key": "failed-orders",
})

Use cases:

  • Poison messages that fail every retry → DLX → DLQ → human investigation.
  • Time-bounded retries (TTL on retry queue → DLX back to main queue).
  • Failed Celery tasks (Celery + RabbitMQ + DLX pattern).

7. What’s the prefetch count and how do you tune it?

basic.qos(prefetch_count=N) — how many unacked messages the broker pushes to a consumer at once.

  • Too low (1): every message requires a round-trip; throughput suffers.
  • Too high (1000): one slow consumer hogs a thousand messages; the rest sit idle.
  • Typical: 10-100 for short work, 1 for long-running tasks.

For Celery: worker_prefetch_multiplier defaults to 4 × worker_concurrency. Lower it for slow tasks to avoid starvation.

8. What’s the difference between basic.ack, basic.nack, and basic.reject?

  • ack — message processed successfully; broker can delete it.
  • nack — negative ack with multiple and requeue options. With requeue=False, message is dead-lettered (or dropped if no DLX). nack can batch (multiple=True).
  • reject — older API, single message, requeue option.

Default Celery behavior: ack on success, nack with requeue=False on hard failure → DLX picks it up.

9. RabbitMQ vs Kafka — when each?

RabbitMQ Kafka
Mental model broker pushes, consumer acks consumer pulls from log
Throughput ~50k/s per node ~1M+/s per cluster
Routing flexibility high (exchanges) low (partition by key)
Replay message acked = gone offsets within retention; replayable
Best for work queues, RPC, complex routing event streaming, audit logs, log integration

Most companies end up running both: RabbitMQ for Celery tasks, Kafka for cross-service event streaming.

10. How does RabbitMQ achieve HA?

Quorum queues replicate across N nodes via Raft consensus. A queue with N=5 tolerates 2 node failures. Writes go to the leader; replicated to a majority of followers before ack.

For broker discovery: a cluster of RabbitMQ nodes federates via Erlang networking. Clients connect via a load balancer that distributes across nodes (or use rabbitmq-uri with multiple hosts).

Classic mirrored queues (deprecated) replicated to designated followers; less reliable failover semantics than quorum.

11. What’s a publisher confirm?

By default basic.publish doesn’t return anything — fire-and-forget. The broker may not have actually persisted the message.

python
channel.confirm_delivery()
ok = channel.basic_publish(
    exchange="...", routing_key="...", body=...,
    mandatory=True,
)
if not ok:
    # broker didn't confirm; resend or alert
    ...

Confirms add latency (broker waits to confirm) but guarantee durability. For mission-critical work, always enable confirms.

12. What’s the mandatory flag?

mandatory=True on publish: if no queue is bound to the exchange with a matching routing key, the message is returned to the publisher (not silently dropped).

Useful for catching routing-key typos and dead consumers. The Channel must have a basic_return callback registered to receive the bounce.

13. Why are channels not threads-safe?

The AMQP wire protocol is multiplexed across one TCP connection via channels — each channel is a logical session. A single channel processes commands serially; concurrent thread access garbles the stream.

Pattern: one connection per app, one channel per thread/coroutine. Pools manage this.

14. What’s a lazy queue?

A queue that prefers disk over RAM. Default queues cache messages in RAM and only write to disk for durability; under deep backlog, the broker OOMs.

python
channel.queue_declare(queue="bulk", arguments={"x-queue-mode": "lazy"})

Use for queues that may accumulate large backlogs of large messages. Slightly slower per-message but won’t blow up the broker.

In modern RabbitMQ (3.12+), quorum queues have similar storage semantics and lazy queues are less common.

15. How do you handle a poison message?

A poison message fails every attempt — malformed payload, missing reference, etc.

Pattern:

  1. Consumer fails, nack with requeue=False.
  2. Configured DLX routes the message to a DLQ.
  3. Alert on DLQ depth growth.
  4. Human inspects: fix the bug, or delete the message if unfixable.
  5. Replay the DLQ (shovel back to main queue) once fixed.

Without DLX, a poison message either bounces forever (if requeue=True) or vanishes silently. Always configure DLX for production queues.

16. What’s RabbitMQ’s “shovel”?

A built-in plugin that copies messages from one queue/exchange to another — possibly on a different broker. Used for:

  • Cross-data-center replication of specific queues.
  • Replaying a DLQ back to the main queue.
  • Migrating queues between brokers.
bash
rabbitmq-plugins enable rabbitmq_shovel

Configure via management UI or rabbitmq.conf.

17. RabbitMQ vs Celery — what’s the relationship?

Celery is a task queue library; RabbitMQ is one of its supported brokers (Redis, SQS, others also work). Celery uses RabbitMQ exchanges + queues internally:

  • Each task queue is a RabbitMQ queue.
  • Routing keys distinguish task names.
  • DLX configuration on the Celery queue enables the DLQ pattern.

Many “Celery with RabbitMQ” production issues are actually RabbitMQ tuning issues — prefetch, durability, DLX.

18. Can you do RPC with RabbitMQ?

Yes — the classic AMQP RPC pattern:

  1. Client declares an exclusive temporary reply queue.
  2. Client sends request with reply_to (the temp queue) and a correlation ID.
  3. Worker processes, publishes reply to the reply_to queue with the correlation ID.
  4. Client consumes from the temp queue, matches by correlation ID.
python
result = channel.queue_declare(queue="", exclusive=True)
reply_queue = result.method.queue
corr_id = str(uuid.uuid4())

channel.basic_publish(
    exchange="", routing_key="rpc_queue",
    properties=pika.BasicProperties(reply_to=reply_queue, correlation_id=corr_id),
    body=request,
)

# Consume from reply_queue, match correlation_id

Rarely the right tool — HTTP/gRPC is usually clearer. Use RabbitMQ RPC when you want broker-mediated load distribution + queue-based backpressure.

19. How does message TTL work?

Two scopes:

  • Per-queue: x-message-ttl on queue declare. Every message expires after N ms.
  • Per-message: expiration on publish, as a string of ms.

Expired messages are dropped (or dead-lettered if DLX configured). Useful for:

  • Cache invalidations that go stale.
  • Time-sensitive notifications.
  • Retry-with-delay patterns (TTL on retry queue → DLX back to original).

20. Production tips?

  • Always durable=True and delivery_mode=2 for production.
  • Use quorum queues, not classic mirrored.
  • Configure DLX on every operational queue.
  • Set basic.qos(prefetch_count=...) consciously per consumer.
  • Publisher confirms for important publishes.
  • Monitor: queue depth, message rate, unacked count, connection count.
  • Connection per app, channel per thread.
  • Don’t rely on memory queues for large backlogs — quorum or lazy.

Mental model

Concept Quick definition
Connection TCP connection to broker; per app
Channel logical session on connection; per thread
Exchange routes messages; producer publishes here
Queue holds messages; consumer reads here
Binding rule connecting exchange → queue
Routing key string used by exchange to decide routing
DLX exchange for dead-lettered messages
Prefetch how many unacked messages per consumer
Quorum queue Raft-replicated, modern HA
Stream append-only log, like Kafka topic

Interview angle 4

  • “Explain exchanges and bindings.” - producers publish to an exchange, never directly to a queue. The exchange type decides routing: direct matches the routing key exactly, topic matches patterns, fanout copies to every bound queue, headers matches on attributes. Bindings connect exchanges to queues.
  • “How do you avoid losing messages?” - durable exchanges and queues, persistent messages, publisher confirms on the producer side, and manual acknowledgement after processing on the consumer side. Missing any one of those reintroduces a loss window.
  • “How do you handle a poison message?” - a dead-letter exchange after a bounded number of redeliveries, so one unprocessable message doesn’t block the queue or loop forever. Pair it with an alert on DLQ depth.
  • “RabbitMQ or Kafka?” - RabbitMQ for routing flexibility, per-message acknowledgement and task distribution; Kafka for high-throughput ordered event streams with replay. They solve different problems despite overlapping.