RabbitMQ Interview Questions · 2026

RabbitMQ Interview Questions (2026): Must-Know Q&A

RabbitMQ shipped its first release back in 2007, and it's still running in production at a huge share of the fintech, logistics, and e-commerce backends that never bothered to switch to something newer. The bigger news for anyone prepping right now: RabbitMQ 4.0, released September 18, 2024, removed classic queue mirroring entirely. Every team still running mirrored classic queues had to move to quorum queues, and interviewers have noticed. Questions about replication and high availability now assume you know what a quorum queue is, not what "HA policy" used to mean (RabbitMQ Docs, Quorum Queues).

Here's an opinion that might get pushback: I think most RabbitMQ prep spends too long diagramming exchange types, because they're visually satisfying and easy to turn into a slide, and not nearly long enough on acknowledgements and prefetch, which is where actual production incidents happen. You can misdraw a fanout exchange on a whiteboard and still ship a working system. Get prefetch wrong on a slow consumer and you'll page someone at 2am.

This page covers RabbitMQ interview questions across nine areas: the AMQP model itself, the four exchange types, queues and bindings and routing keys, how producers and consumers actually move a message, acknowledgements and redelivery, durability and persistence, prefetch and QoS, dead-letter exchanges and TTL, and clustering with quorum queues, closing with the RabbitMQ-versus-Kafka question that comes up in almost every senior loop. Code examples use Python with the pika client, since it's the library most interview take-homes and whiteboard sessions default to, plus bash for the CLI tools (rabbitmqctl, rabbitmqadmin) that come up in operations-heavy rounds.

52Questions
Exchanges, Acks & Quorum QueuesCore Topic
Config, CLI & PythonFormat
250Default Prefetch

The AMQP model: why RabbitMQ routes instead of just queuing

Every loop starts here, even for a candidate who's run RabbitMQ in production for years. Skip it and you'll fumble a follow-up two questions later.

Easy questions

15

AMQP (Advanced Message Queuing Protocol) is a wire-level messaging protocol, a specification for how bytes move between a client and a broker, not a library or a product. RabbitMQ was built as a broker for AMQP 0-9-1, and as of version 4.0 it also treats AMQP 1.0 as a core, always-enabled protocol rather than a plugin.

The distinction matters because AMQP defines the exchange-binding-queue model directly. That's different from something like Kafka, which never adopted AMQP and built its own protocol around an append-only log instead.

Producer publishes a message to an exchange, never directly to a queue. The exchange looks at its bindings (rules that connect it to one or more queues, usually keyed on a routing key) and copies the message into whichever queues match. A consumer subscribed to one of those queues then receives it.

A common beginner mistake is treating the exchange as a formality and mentally routing straight from producer to queue. Once you're debugging a message that "disappeared," the first question is always which exchange it hit and which binding, if any, actually matched (RabbitMQ Docs, AMQP 0-9-1 Model Explained).

Exact match. A direct exchange routes a message to any queue whose binding key matches the message's routing key character for character. Bind a queue with the key "payment.failed" and only messages published with that exact routing key land there, nothing close, nothing partial.

A binding is the rule connecting an exchange to a queue, made up of the exchange, the queue, and (for direct and topic exchanges) a binding key to match against incoming routing keys. Yes, a single queue can bind to multiple exchanges, and multiple queues can bind to the same exchange with different keys, which is exactly how fanout-style broadcast plus targeted delivery coexist on the same topic exchange.

Push, by default. Once a consumer registers with basic.consume, RabbitMQ delivers messages to it as they become available, up to whatever the prefetch limit allows. There's also a pull-style basic.get, which fetches a single message on demand, but it's meant for occasional polling or debugging, not a production consumption loop; it doesn't batch or stream and it's noticeably less efficient at any real volume.

With auto-ack, RabbitMQ considers a message delivered and removes it from the queue the instant it's sent over the wire, before your code has done anything with it. If the consumer crashes mid-processing, that message is gone. Not requeued, not redelivered, just gone, because the broker already marked it as successfully handled.

Manual acknowledgment defers that removal until your code explicitly calls basic.ack, so a crash between receipt and processing means the message stays in the queue (or gets redelivered once the connection drops) instead of vanishing.

A durable queue survives a broker restart as an empty queue, its definition gets rewritten to disk, but that says nothing about what's inside it. A persistent message (delivery_mode=2) tells RabbitMQ to write that specific message to disk as it's stored.

Declare a durable queue but publish non-persistent messages, and a restart leaves you with the queue intact and completely empty. Publish persistent messages into a non-durable (transient) queue, and the queue itself disappears on restart, message durability included, since there's no queue left to hold them.

It caps how many unacknowledged messages RabbitMQ will hand a single consumer before it stops sending more and waits for acks to come back. Leave it unset and RabbitMQ applies a default of 250 messages per consumer, which is fine for tiny, fast jobs and can be a real problem for anything slow (RabbitMQ Docs, Consumer Prefetch).

One quirk worth knowing: RabbitMQ deviates from strict AMQP 0-9-1 behavior here. By default the limit applies per consumer, not shared across every consumer on a channel, unless you explicitly set the global flag.

A DLX is just a regular exchange you designate, on a queue, as the destination for messages that queue can't or won't keep. Three things trigger it: a consumer rejects or nacks a message with requeue=false, a message's TTL expires before anyone consumes it, or a queue hits its max-length limit and starts dropping the oldest messages to make room.

A fanout exchange ignores the routing key entirely and delivers a copy of every message to every queue bound to it. There's no matching logic at all, it just broadcasts. You'd reach for it when you want every consumer group to see the same event: a user.created event that needs to be picked up independently by an email service, an analytics service, and a fraud-check service. Each service binds its own queue to the fanout exchange with no routing key needed, and RabbitMQ copies the message to all three.

A direct exchange, by contrast, only delivers to queues whose binding key exactly matches the routing key, which is useful for point-to-point work distribution but wrong for broadcast semantics. In practice, fanout is the exchange type behind most pub/sub implementations in RabbitMQ, and it's also the simplest exchange to reason about because there's nothing to get wrong in the binding.

A vhost is a logical namespace inside a single RabbitMQ broker. Exchanges, queues, and bindings created in one vhost are completely invisible to another vhost, and each vhost has its own set of user permissions. It's effectively multi-tenancy built into the broker itself, one cluster can host separate vhosts for staging and production, or for two unrelated teams, without them colliding on queue names or accidentally consuming each other's messages.

The other practical reason to split vhosts is permission scoping. You can give a CI pipeline configure, write, and read access to a "ci" vhost and nothing else, so a bad deploy script can't touch production queues even if the credentials leak. Most default installs never go beyond the "/" vhost, but once you're running shared infrastructure for multiple apps or environments, vhosts are the cheap way to keep them isolated without spinning up separate clusters.

A connection is a real TCP socket, usually wrapped in TLS, between your app and the broker. Opening one is expensive: there's a TCP handshake, an AMQP handshake, and the broker spins up an Erlang process to manage it. A channel is a lightweight virtual connection multiplexed inside that single TCP connection. Almost everything you actually do, publishing, consuming, declaring queues, acking, happens on a channel, not directly on the connection.

The practical rule is one connection per process, one channel per thread or per unit of concurrent work. Channels are cheap to open and close, connections aren't. A common mistake is opening a new connection for every publish, which burns file descriptors and CPU on both sides for no reason. The fix is to open one connection at startup and hand out channels from a pool as needed.

It's the web UI and HTTP API that ships with RabbitMQ but isn't enabled by default, you turn it on with rabbitmq-plugins enable rabbitmq_management. Once it's on you get a dashboard on port 15672 showing every exchange, queue, connection, and channel, along with live message rates in and out. You can also create and delete queues and exchanges, manually publish a test message, purge a queue, and manage users and permissions, all without touching a client library.

Beyond the UI, the same functionality is exposed as an HTTP API, which is what most monitoring integrations actually scrape instead of parsing the HTML. In production you'd usually lock the management UI behind a VPN or reverse proxy since it can see message payloads and cluster topology, and you'd give read-only, monitoring-tagged users to anyone who just needs visibility rather than the ability to delete a queue by accident.

Exclusive means the queue is tied to the connection that declared it. Only that connection can use it, and RabbitMQ deletes the queue the moment that connection closes, whether it closed cleanly or crashed. Auto-delete means the queue sticks around independent of any single connection, but gets deleted once its last consumer unsubscribes, even if the connection that declared it is still open.

They're often used together for temporary, per-client queues, like a reply queue for an RPC call, but they solve different problems. Exclusive protects against another process accidentally attaching to a queue meant to be private. Auto-delete protects against orphaned queues piling up after every consumer has walked away. A queue can be both, neither, or just one, and it's worth being deliberate about which one you actually need instead of reflexively setting both.

RabbitMQ is fundamentally asynchronous, so doing synchronous request and response over it means building the pattern yourself. The client declares a temporary, usually exclusive, reply queue, then publishes the request with the reply-to property set to that queue's name and a correlation-id set to some unique value it generates. The server processes the request and publishes the response to whatever queue is named in reply-to, copying the same correlation-id back onto the response.

The client is meanwhile consuming from its reply queue, and when a message shows up it checks the correlation-id against the ones it's waiting on to match the response to the right in-flight request.

python
result = channel.queue_declare(queue="", exclusive=True)
callback_queue = result.method.queue

channel.basic_publish(
  exchange="",
  routing_key="rpc_requests",
  properties=pika.BasicProperties(
    reply_to=callback_queue,
    correlation_id=str(uuid.uuid4()),
  ),
  body=request_body,
)

Correlation-id matters because a single reply queue can have multiple outstanding requests in flight at once. It works, but it's genuinely slower and operationally heavier than a plain HTTP call, so most teams only reach for it when the request specifically needs to flow through the same broker and topology as everything else.

Medium questions

25

A topic exchange does pattern matching instead of exact matching. Routing keys are dot-separated words, and a binding key can use * to match exactly one word or # to match zero or more words.

Bind with orders.*.created and you'll catch orders.us.created and orders.eu.created, but not orders.us.eu.created (too many words for the single *) and not orders.created (missing the middle word entirely). Bind with orders.# instead and both of those match, since # swallows any number of words including zero.

The routing key travels with the message, set by the producer at publish time. The binding key lives on the binding itself, set when a queue subscribes to an exchange. Routing happens when the exchange compares the message's routing key against every binding key registered on it.

For a direct exchange they need to match exactly. For a topic exchange the binding key is the pattern (with * and #) and the routing key is the literal string being tested against it. People use the two terms interchangeably in casual conversation and it rarely causes confusion until a topic exchange is involved.

A plain basic.publish is fire-and-forget from the client's point of view, TCP accepting the bytes doesn't mean the broker actually stored the message. Publisher confirms have the broker send an asynchronous acknowledgment back to the producer once the message is safely handled (written to disk for a persistent message, or replicated to enough quorum queue members), so the producer knows definitively whether it needs to retry.

Without confirms, a broker crash or a routing failure (publishing to an exchange with no matching binding) can silently drop a message the application thinks it already sent. Confirms don't fix that on their own, you still need to handle the negative or missing acknowledgment, but they at least make the failure visible instead of silent.

basic.reject only ever handles one message at a time. basic.nack is RabbitMQ's own extension to the protocol and adds a multiple flag, letting you negatively acknowledge every unacked message up to and including the one you're pointing at in a single call, which matters if you're batching processing and something fails partway through a batch.

Both take a requeue flag. Set it true and the message goes back on the queue (or straight to a different consumer, since a single consumer isn't guaranteed to see it again). Set it false, and if a dead-letter exchange is configured, the message routes there instead of disappearing.

No, and I think this is the most overclaimed word in RabbitMQ marketing copy. Durability protects against a clean restart or a planned broker shutdown. It doesn't protect a message that's still in flight over the network when the broker crashes, and on a classic queue running on a single node, it doesn't protect against that node's disk failing before the write actually lands.

Getting close to zero loss takes three things together: persistent messages, a quorum queue (so the message replicates to a majority of nodes, not just one disk), and publisher confirms on the producer side so the app actually knows the write succeeded before it moves on. Any one of the three missing and you've got a gap somewhere.

RabbitMQ dispatches round-robin at delivery time, not based on how fast each consumer is actually working through its backlog. With prefetch at 250, one consumer can end up holding 250 unacked messages, chewing through them slowly, while the other consumer's prefetch buffer sits empty because the round-robin already handed out that batch before either consumer's actual processing speed became visible.

Prefetch 1 fixes the imbalance since a consumer can't be handed a new message until it acks the last one, but it's not free. Every single message now waits on a full network round trip for the ack before the next one gets dispatched, which tanks throughput for short, uniform, fast jobs where the round-trip latency dwarfs the actual processing time. I'd set prefetch low (1 to 10) for slow, uneven work and higher for fast, uniform work, not reflexively set it to 1 everywhere because a blog post said so once.

Message TTL (x-message-ttl on the queue, or per-message via the expiration property) controls how long an individual message sits unconsumed before it expires. Queue TTL, more precisely x-expires, controls how long the entire queue can sit with zero consumers before RabbitMQ deletes the whole queue, not just its messages.

Yes, you can set both, and they solve different problems: message TTL cleans up stale data inside a live queue, queue TTL cleans up abandoned queues nobody's listening to anymore, which matters more than it sounds like once you've got auto-generated, per-connection reply queues piling up.

A quorum queue replicates its data across multiple cluster nodes using the Raft consensus algorithm, electing a leader and requiring a majority of replicas to agree before a write counts as committed. Classic mirrored queues copied data too, but with a weaker, less formally specified replication model that RabbitMQ deprecated starting in version 3.9 and removed entirely in 4.0.

Practically: quorum queues need an odd number of replicas (3 is typical) to have a clean majority, they don't support every classic-queue feature (priority queues work differently, and per-message TTL has some edge cases), but they're the only replicated option going forward, so that's the trade you're making either way.

Out of the box, with auto-ack and no publisher confirms, RabbitMQ effectively gives you at-most-once, a message can silently vanish on a crash and nothing retries it. Turn on manual acknowledgments, publisher confirms, and a quorum (or at minimum durable) queue, and you get at-least-once: a crash means reprocessing, not loss, so your consumer needs to tolerate seeing the same message twice.

RabbitMQ doesn't offer a built-in exactly-once guarantee the way Kafka Streams does for Kafka-to-Kafka pipelines. If you need exactly-once semantics, you're building idempotency yourself, usually a unique message ID plus a dedupe check on the consumer side, because the broker isn't going to hand it to you.

A normal classic queue tries to keep messages in memory for speed, only paging them to disk under memory pressure. A lazy queue, set via the queue argument x-queue-mode: lazy, writes messages to disk as soon as they arrive and only pulls them into memory when a consumer is actually about to receive them.

You reach for lazy behavior when a queue is expected to build up a large backlog, millions of messages during a consumer outage or a slow downstream batch job, and you don't want that backlog to push the node into a memory alarm and start blocking publishers across the whole vhost. The tradeoff is throughput: lazy queues are measurably slower for normal steady-state traffic because every message round-trips through disk instead of staying in memory. In practice you'd use lazy for a queue that's explicitly a buffer or dead-letter sink, and leave hot-path queues on default behavior. Quorum queues, which RabbitMQ now steers you toward for anything durable, already behave lazily by design, so this distinction mostly matters for classic queues.

Single active consumer lets you attach multiple consumers to the same queue but have RabbitMQ only actually deliver messages to one of them at a time. The others sit idle as hot standbys. If the active consumer's connection drops, RabbitMQ promotes the next registered consumer automatically, with no manual failover logic on your side.

It solves the problem of needing ordered, exclusive processing, you don't want two instances of a service racing to process the same stream of events, while still wanting high availability if one instance dies. Without it, your options were either a genuinely exclusive queue, which has no failover at all, or round-robin dispatch across consumers, which breaks ordering the moment you have more than one worker. It's set with the x-single-active-consumer queue argument, and it works on both classic and quorum queues, though it's most commonly paired with quorum queues since those already give you the durability half of the story.

A consistent hash exchange, a plugin that ships with the broker, hashes the routing key of each message and uses that hash to pick one of the bound queues, with the binding's numeric argument acting as a weight for how much traffic that queue should get relative to the others. The point is to spread load across a set of queues while keeping all messages for the same key landing on the same queue every time, which matters if you need per-key ordering, like all events for a given user ID going to the same worker.

A direct exchange can also route deterministically, but you'd need one binding key per queue and would have to manually assign every possible key up front, which doesn't work when your key space is unbounded, arbitrary user IDs for instance. Consistent hashing gives you the same "same key, same queue" guarantee without enumerating keys, and because it's consistent hashing specifically, adding or removing a queue only reshuffles a small fraction of the key space instead of remapping everything, similar to reasoning about a hash ring in a distributed cache.

RabbitMQ watches its own memory usage against vm_memory_high_watermark, a fraction of total system memory, 0.4 by default, and disk free space against disk_free_limit. When either threshold is crossed, the node fires a resource alarm, and the practical effect is blunt: RabbitMQ blocks all publishing connections across the entire node, or potentially across the vhost in a cluster, until usage drops back under the limit. Consumers keep working, so the queue keeps draining, but nothing new can come in.

This is a deliberate, if occasionally painful, backpressure mechanism. Without it, an unbounded publisher on a queue with no consumers would eventually crash the broker outright by exhausting memory or disk. The gotcha in production is that this is node-wide, not queue-specific: one badly behaved queue building an enormous backlog can trip the memory alarm and block publishers for every other, completely unrelated queue on that node. That's usually the argument for lazy queues or per-queue length limits on anything that might build an unbounded backlog, rather than relying on the global alarm as your only safety net.

An alternate exchange is a policy or argument you set on an exchange, alternate-exchange, that tells RabbitMQ where to send a message when it can't be routed anywhere, meaning no binding matches the routing key at all. Instead of the message silently vanishing, the default behavior for a non-mandatory publish with no match, it gets redirected to the alternate exchange, which you can bind a queue to and use to catch anything that fell through.

The difference from a catch-all binding, like binding a queue with routing key # on a topic exchange, is that a catch-all binding lives on the same exchange and competes with your real bindings using the same matching rules, so it can accidentally catch messages you did intend to route elsewhere depending on binding precedence. An alternate exchange only fires for messages with literally zero matching bindings on the original exchange, which makes it a cleaner way to catch genuinely orphaned messages, like a mistyped routing key from a producer, without risking double delivery to a queue that also matched a real binding.

Every unacknowledged message stays fully in memory on the broker, or paged to disk under pressure for a lazy or quorum queue, until it's acked, nacked, or the consumer's connection drops, at which point it gets requeued. There's no automatic timeout on classic queues by default, an unacked message can sit there indefinitely if the consumer just never acks it, holding memory the whole time. This is exactly what prefetch is meant to bound, capping how many unacked messages a single consumer can hold at once.

The real risk shows up when a consumer has a bug that swallows exceptions after receiving a message but before acking it, or acks in a background thread that occasionally dies silently. Unacked count creeps up slowly, memory climbs, and eventually you either hit a memory alarm or, worse, the consumer's connection drops and thousands of messages get redelivered all at once, which can look like a traffic spike or a duplicate-processing incident if your consumer isn't idempotent. Quorum queues added a consumer timeout, delivery-limit and consumer-timeout settings, specifically to force a decision on messages that sit unacked too long, which classic queues never had.

x-max-length is a queue argument that caps the number of messages, or x-max-length-bytes for total size, a queue will hold. Once the queue is at the limit, RabbitMQ has to decide what to do with the next incoming message, and that's controlled by the overflow argument.

python
channel.queue_declare(
  queue="orders",
  durable=True,
  arguments={
    "x-max-length": 100000,
    "x-overflow": "reject-publish"
  }
)

The default, drop-head, silently discards the oldest message at the front of the queue to make room for the new one, treating the queue like a ring buffer. reject-publish instead refuses the new message: if the publisher used publisher confirms, it gets a nack; if the exchange routing was mandatory, it can come back as a basic.return. drop-head is fine for something like a live telemetry feed where the newest data point matters more than an old one nobody read yet. reject-publish is the right call for anything where losing a message silently would be a correctness bug, like an order queue, because it pushes the backpressure decision back to the producer instead of quietly eating data.

mandatory is a flag you set on basic.publish that tells RabbitMQ you want to know if the message couldn't be routed to any queue at all, instead of it just silently disappearing, which is the default behavior. If mandatory is true and no binding matches the routing key on that exchange, RabbitMQ sends the message back to the publisher as a basic.return, including the reply code and text explaining why, over the same channel.

It only fires for a complete routing failure. If the message reaches at least one queue it's considered delivered even if that queue later has no consumers or eventually drops the message some other way. In practice you register a return listener on the channel before publishing, and treat a return as a signal that something's misconfigured, a typo'd routing key, a binding that got deleted, an exchange missing a catch-all. It's a cheap, useful safety net for catching silent routing bugs early, though it adds a small overhead per publish, so some teams only enable it in specific high-value producers rather than everywhere.

Both move messages between brokers, but they solve different shaped problems. Shovel is essentially a dedicated client that connects a source queue on one broker to a destination exchange on another, and just pumps messages across, one directional pipe you explicitly configure. It's simple and predictable, good for one-off or small-scale bridging, like draining a queue from an old cluster into a new one during a migration, or forwarding a specific queue's traffic to a different environment.

Federation is built for keeping exchanges, or queues, loosely in sync across geographically or organizationally separate brokers, and it's aware of topology in a way Shovel isn't. It can federate an exchange so that bindings made on the downstream broker cause messages to flow from the upstream one automatically, without hand-wiring every route. Federation is the better fit when you're running multiple clusters across regions and want messages published in one region to reach consumers bound in another, without those clusters being one tightly-coupled cluster. Shovel is the better fit for a fixed, known point-to-point transfer.

Setting x-max-priority on a queue lets you attach a priority value, 0 up to that max, to individual messages via the priority property, and RabbitMQ will deliver higher-priority messages before lower-priority ones that arrived earlier, instead of strict FIFO. It sounds like an obviously good feature for anything with an urgent flag, like a password-reset email jumping ahead of a marketing newsletter in the same queue.

The tradeoffs are real, though. Internally RabbitMQ implements this by maintaining a separate sub-queue per priority level, so setting x-max-priority to something large, 100 for instance, when you only actually use two or three distinct priorities creates unnecessary overhead for no benefit. It also complicates ordering guarantees: messages of the same priority still come out in order relative to each other, but overall ordering across the queue is no longer purely FIFO, which can surprise consumers written assuming strict ordering. Most teams get better mileage out of running two separate queues, one high-priority and one normal, with more workers assigned to the high-priority one, than from trying to encode priority within a single queue.

Every node in a cluster stores queue and message data according to the queue type's own durability rules, that part doesn't change. What disc vs RAM controls is where the cluster's metadata lives, exchange definitions, bindings, user accounts, vhosts, and policies. A disc node writes that metadata to disk and survives a restart with it intact. A RAM node keeps that same metadata only in memory, which makes metadata operations faster on that node, but means it has nothing to recover from if it restarts alone, it has to rejoin the cluster and resync from a disc node.

The catch that trips people up is that a cluster needs at least one disc node at all times, and the traditional guidance was to run at least two so you're never one node's restart away from losing all cluster metadata permanently. In practice, most production clusters just run all nodes as disc nodes now since the performance difference is small for most workloads and the operational risk of getting the mix wrong isn't worth the marginal gain. RabbitMQ's move to Khepri as the metadata store is partly aimed at making this distinction less relevant going forward.

Most modern client libraries, the Java client, the.NET client, and popular wrappers around the Python and Node clients, ship with automatic connection recovery built in, usually on by default. When the TCP connection drops, whether from a network blip, a broker restart, or a load balancer timing out an idle connection, the client detects the failure, often via a missed heartbeat, waits a configurable backoff interval, and reopens the connection.

The part that actually matters operationally is what recovery re-declares automatically: exchanges, queues, and bindings that the client declared itself get recreated, and consumers get re-registered against their original queues so message flow resumes without your application code noticing anything beyond a gap in delivery. What it does not save you from is in-flight, unacknowledged messages at the moment of disconnect, those get requeued by the broker and redelivered once the consumer reconnects, so your consumer logic still needs to be safe to receive the same message twice. Teams sometimes disable automatic recovery to hand-roll their own reconnect logic with custom monitoring hooks, but the default behavior covers the common case well enough that it's rarely worth reimplementing.

Both exist to give a publisher certainty that a message actually made it to the broker, but they work completely differently and have very different performance profiles. tx.select puts the channel into transactional mode, and you publish messages, then explicitly call tx.commit, which blocks until the broker confirms every message in that transaction was persisted. It's synchronous, one transaction at a time per channel, which makes it correct but slow, you're doing a round trip per batch and can't pipeline the next publish until the commit returns.

Publisher confirms, confirm.select, instead put the channel into a mode where the broker asynchronously acks or nacks each published message by its sequence number, and you can have thousands of confirms outstanding at once without blocking between publishes. You track which sequence numbers are still unconfirmed and only consider a message safely delivered once its confirm comes back. In practice nobody uses transactions for throughput-sensitive publishing anymore, confirms give the same delivery guarantee with dramatically better throughput because they don't force a request and response round trip per message. Transactions still exist mostly for backward compatibility and for the rare case needing atomicity in a single unit that also includes queue operations, not just publishes.

Within a single queue with a single consumer, RabbitMQ delivers messages in the order they were enqueued, and that holds for both classic and quorum queues. The moment you add a second consumer on the same queue, ordering across the whole queue is gone, because messages get dispatched round-robin, subject to prefetch, across consumers, and there's no guarantee the second consumer finishes processing message 2 before the first consumer finishes message 4.

Requeues also break strict ordering: if a message gets nacked or redelivered after a consumer crash, it goes back into the queue, but not necessarily at the front, depending on queue type and version, so a redelivered message can end up processed after messages that were originally behind it. Priority queues explicitly break FIFO by design. If you need per-key ordering with concurrency, all events for order ID 123 processed in order but different order IDs processing in parallel, the actual pattern is to route by key to multiple queues, using something like a consistent hash exchange, and keep exactly one consumer per queue, rather than trying to get ordering out of one queue with many consumers.

A heartbeat is a small, empty AMQP frame that client and broker exchange periodically just to prove the TCP connection is still alive and the other side is still processing frames, not just holding a socket open. The negotiated heartbeat interval, 60 seconds by default in recent versions, means if either side doesn't see a heartbeat, or any traffic, within roughly two intervals, it assumes the connection is dead and closes it.

Set it too low and you get false positives: a client under heavy GC pressure or a broker doing a big compaction pass can miss the deadline for reasons that have nothing to do with the connection actually being broken, and you end up with connections flapping under load, which is worse than the problem you were trying to catch. Set it too high, or disable it entirely, and a genuinely dead connection, say a NAT gateway silently dropped the mapping, can sit open for a long time before either side notices, during which publishes might appear to succeed locally while never reaching the broker. The common production setting is somewhere around 30 to 60 seconds, tuned to be comfortably longer than your worst-case GC pause or blocking I/O stall, but short enough to detect real failures within a reasonable window.

Normally you bind a queue to an exchange, but RabbitMQ also lets you bind one exchange directly to another exchange, using the same routing-key matching rules as a queue binding would. A message published to the source exchange gets evaluated against the binding and, if it matches, forwarded to the destination exchange, where it goes through that exchange's own routing logic against its own bindings.

A real use case is layering a topic exchange for flexible routing on top of a simpler exchange that producers already publish to, without changing what producers do. Say you have an existing direct exchange that services publish orders.created events to, and you want to add topic-based fan-out to multiple new consumer teams without touching every producer's code. You create a new topic exchange, bind it to the existing direct exchange with the relevant routing key, and now anyone can bind their own queue to the topic exchange with wildcard patterns, layering richer routing on top of an exchange you don't want to modify or that other services already depend on staying exactly as it is.

Hard questions

12

A headers exchange ignores the routing key entirely and matches on message header attributes instead, using x-match=all (every header must match) or x-match=any (one match is enough). It's useful when the routing decision depends on more than one independent attribute that doesn't compress cleanly into a dot-separated string.

My honest take: I've seen exactly one production headers exchange in years of looking at other teams' RabbitMQ configs, and it existed because someone needed to route on both region and priority independently. Everyone else just concatenates the attributes into a topic routing key instead, because it's easier to debug when you can read the routing key straight off a log line instead of digging through headers.

All three. orders.# matches because # swallows any number of trailing words. orders.*.created matches because there's exactly one word (us) between orders and created. orders.us.created matches because it's an exact literal match, which topic exchanges still honor.

bash
rabbitmqadmin declare binding source=orders_topic destination=q_all 
 routing_key="orders.#"

rabbitmqadmin declare binding source=orders_topic destination=q_created 
 routing_key="orders.*.created"

rabbitmqadmin declare binding source=orders_topic destination=q_exact 
 routing_key="orders.us.created"

rabbitmqadmin publish exchange=orders_topic routing_key="orders.us.created" 
 payload="order 4471 created"

The part candidates miss: a topic exchange doesn't pick the "best" or most specific match and stop there. It evaluates every binding independently and delivers a copy of the message to every queue whose binding matches, full stop.

Requeuing with no limit creates exactly the loop you'd expect: broker redelivers, consumer fails again, consumer nacks with requeue=true again, and nothing ever breaks the cycle on its own. The traditional fix is to stop requeuing after N attempts, track a retry counter yourself (in a header, or in an external store), and nack with requeue=false once you've hit the limit, routing the message to a dead-letter exchange instead.

python
def on_message(channel, method, properties, body):
  headers = properties.headers or {}
  attempts = headers.get("x-retry-count", 0)

  try:
    process(body)
    channel.basic_ack(delivery_tag=method.delivery_tag)
  except Exception:
    if attempts >= 3:
      # give up: dead-letter it instead of looping forever
      channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
    else:
      channel.basic_publish(
        exchange="",
        routing_key=method.routing_key,
        body=body,
        properties=pika.BasicProperties(
          headers={"x-retry-count": attempts + 1}
        ),
      )
      channel.basic_ack(delivery_tag=method.delivery_tag)

Yes, it's gotten a little easier: quorum queues in RabbitMQ 4.0 ship with a built-in delivery limit, defaulting to 20 redeliveries before the message dead-letters automatically, no manual counter required (RabbitMQ Docs, Quorum Queues). Classic queues never got that feature, which is one more reason quorum queues are the default recommendation for anything new.

Set global_qos to False (the pika default) and each new consumer registered on that channel gets its own independent budget of unacked messages, rather than sharing one pool across all consumers on the channel.

python
channel.basic_qos(prefetch_count=5, global_qos=False)

channel.basic_consume(
  queue="orders",
  on_message_callback=on_message,
  auto_ack=False,
)

channel.start_consuming()

Flip global_qos to True and that number of 5 becomes a shared ceiling across every consumer using the same channel, which is almost never what you want if you're running multiple consumers per connection for throughput.

The trick people call the "parking lot" pattern: a failed message doesn't go straight back to the main queue, it gets nacked into a retry queue that has a TTL and no consumers. Once each message's TTL expires there, RabbitMQ dead-letters it again, this time back into the original queue, giving you a delay without any external scheduler or plugin.

python
channel.queue_declare(
  queue="orders.retry",
  durable=True,
  arguments={
    "x-message-ttl": 30000, # wait 30s before retrying
    "x-dead-letter-exchange": "",
    "x-dead-letter-routing-key": "orders",
  },
)

channel.queue_declare(
  queue="orders",
  durable=True,
  arguments={
    "x-dead-letter-exchange": "",
    "x-dead-letter-routing-key": "orders.retry",
  },
)

Both queues point at each other through the default (nameless) exchange's dead-letter routing. Nobody consumes from orders.retry directly, its whole job is to hold a message for exactly one TTL window and then bounce it back.

The two nodes that can still see each other keep a majority (2 out of 3), so they can elect or keep a leader and keep accepting writes normally. The isolated single node can't reach a majority on its own, so it can't serve as leader for that queue and effectively stalls for that partition until the network heals and it rejoins.

That's Raft doing its job: sacrificing availability on the minority side to guarantee the majority side never diverges into two conflicting versions of the truth. I don't have a clean number on how many teams have actually finished their migration off classic mirrored queues since the 4.0 cutover in late 2024; anecdotally, it's a mixed bag, and some are clearly still catching up.

The real distinction is where the intelligence lives. RabbitMQ is a smart broker, dumb consumer model: the broker does the routing (exchange types, bindings, priority, per-message TTL), tracks what's been acked, and once a message is consumed and acked, it's gone. Kafka flips that. It's a dumb broker, smart consumer model built around an append-only log: the broker just stores an ordered sequence of records and keeps them around for a configured retention window, and consumers track their own offset and can rewind and replay whenever they want.

That difference explains almost every practical trade-off people argue about. Need complex routing, per-message priority, or a fine-grained retry and DLX story? RabbitMQ's model is built for exactly that and Kafka makes you build it yourself on top of the log. Need to replay a week of history to rebuild a downstream cache, or sustain genuinely enormous throughput with many independent consumer groups reading the same stream? That's what Kafka's log model exists for, and RabbitMQ doesn't naturally do "replay everything since Tuesday" once a message has been acked and removed. My honest take: comparing them feature-by-feature past this point mostly wastes interview time. The one-sentence version an interviewer actually wants is "smart broker versus dumb broker, and the log versus the queue."

Mnesia is the Erlang-native distributed database RabbitMQ has always used to store cluster metadata, queue and exchange definitions, bindings, users, and so on, across all nodes. It's been a persistent source of pain in production clusters because Mnesia doesn't handle network partitions gracefully: it has no built-in consensus protocol, so when a partition heals, Mnesia can end up with genuinely inconsistent state across nodes, and recovering from that historically meant manually deciding which node's view of the world to keep and forcibly resetting the others, sometimes with real metadata loss.

Khepri, introduced as the new metadata store starting around RabbitMQ 3.13 and becoming more central in 4.0, is built on Ra, RabbitMQ's own Raft implementation, the same one quorum queues use. Because Raft has actual leader election and log replication with a real consensus guarantee, Khepri-backed clusters don't get into the same kind of split-brain metadata mess Mnesia could, and partition recovery is a well-defined, automatic process instead of a manual incident. The tradeoff during the transition is that Khepri is newer and has had less time in production at scale than three decades of Mnesia, so some operators intentionally stayed on Mnesia through the early 4.0 releases until Khepri's track record caught up, but the direction is clearly toward Khepri as the default going forward.

A quorum queue is really a Raft cluster of its own, one replica per node it's placed on, typically three or five. Every operation that changes the queue's state, a publish landing in the queue, a message getting acked, a requeue, gets appended as an entry to the Raft log by the leader replica, and that entry has to be replicated to and acknowledged by a majority of the other replicas before the leader considers it committed, and before it'll confirm the message back to the publisher if you're using publisher confirms, which you should be with quorum queues.

That's the direct source of the latency difference from classic queues. A classic queue on a single node just needs to write to its own local Erlang process state, and disk if durable, before confirming, no cross-node round trip required. A quorum queue's publish confirm can't come back until a quorum of replicas, potentially on different physical nodes across a network, has durably logged the write. In practice this adds low single-digit milliseconds of latency per publish in a healthy cluster, which is a reasonable price for what you get: the queue survives losing a minority of its replicas, one node out of three, without data loss and without a manual failover, because whichever replicas remain still hold a majority and can keep electing a leader and serving writes.

With cluster_partition_handling set to ignore, when a partition happens, both sides of the split keep running completely independently, accepting publishes and deliveries as if nothing happened, each side unaware the other exists. For classic queues and general cluster-wide metadata, quorum queues have their own Raft-based partition tolerance regardless of this setting, this means you can end up with two divergent views of cluster state, and when the partition heals, RabbitMQ has to reconcile that divergence, which historically has meant picking a winning side and potentially losing data or connections on the other.

pause-minority instead has the side of the cluster that can't see a majority of nodes shut down its own connections and stop accepting operations entirely, essentially sacrificing availability on the minority side to guarantee the majority side stays in a single, consistent, unambiguous state. The tradeoff is real: during a partition, clients connected to whichever nodes end up in the minority get disconnected and can't do anything until the partition heals and that side rejoins, so you're trading some availability for correctness. In most production setups that's the right trade, because the alternative under ignore, silently diverging state that needs manual reconciliation after the fact, tends to be a much worse operational incident than a temporary, well-defined unavailability on a known subset of nodes. pause-minority is really only safe with an odd number of nodes and quorum-aware placement, with two nodes there's no real majority to speak of and you'd want a third node or a different strategy entirely.

A stream is a different underlying data structure from a normal RabbitMQ queue, an append-only log, conceptually much closer to a Kafka partition than to a classic or quorum queue. Where a normal queue deletes a message once it's acked and consumed, a stream retains messages after consumption, up to a configured retention policy based on size or age, and consumers read from an offset they track themselves rather than the broker deleting state as it goes. Multiple independent consumer groups can read the same stream from different points, replay from the beginning, or rewind after a bug, all things a normal RabbitMQ queue fundamentally can't do because messages are gone the moment they're acked.

Streams also use a different protocol at the implementation level for high-throughput cases, the dedicated RabbitMQ Stream protocol rather than plain AMQP 0-9-1, though AMQP clients can still read from streams with reduced performance for compatibility. You'd reach for streams instead of a normal queue when you specifically need replay, large fan-out to many independent consumer groups reading at different speeds, or very high throughput ordered log semantics, the kind of workload people traditionally reached for Kafka for. The honest answer on Streams vs Kafka is that Kafka still has the deeper ecosystem, Kafka Streams, Connect, a huge base of existing tooling, so teams already committed to Kafka for that ecosystem usually stay there. Streams mostly matter for teams already running RabbitMQ for their queue workloads who don't want to operate two entirely separate messaging systems just to get log semantics for one or two use cases.

First thing I'd check is whether idle is actually true or just what the connection list shows. Open the management UI or hit the API for that queue specifically and look at the unacked count, not just the connection state. If unacked is climbing right along with total depth, the consumer isn't actually idle, it's pulling messages and then stalling before acking, silently hung on something downstream, a database call with no timeout, a deadlocked lock, a thread pool that's exhausted so the message sits in a callback that never gets scheduled to run. Prefetch not being exhausted in the naive sense can still mean the consumer's application-level concurrency is maxed out even though the AMQP-level prefetch window has room, if the consumer library pulls messages faster than the app can process them and buffers them internally.

If unacked is low and it's genuinely not consuming, next I'd check whether the consumer is bound to the queue I think it is. A surprisingly common cause is a rebinding or topology change, an exchange got redeclared, a routing key changed, that silently detached the consumer from new messages while its connection stays healthy and shows as connected. I'd also check for a mismatch between routing key and binding key from a recent deploy, and separately check for a second, competing consumer group accidentally bound to a different queue that's draining fine while the one you're watching fills up, which happens more than people expect when queue names get typo'd in config across environments. Last resort, I'd manually get a message off the queue via the management API's basic.get to confirm the data itself isn't malformed in a way that's causing every consumer instance to throw and requeue in a tight loop that looks like idle from a connection-count view but is actually thrashing.

How to prepare for a RabbitMQ interview in 2026

Skip another exchange-type diagram and just run one. Docker gets a single-node RabbitMQ instance up in about two minutes with the management plugin, and from there, break things on purpose: declare a topic exchange with three overlapping bindings and watch which queues actually get a test message, set prefetch to 1 versus 50 and watch throughput change with a simple timer around your consumer loop, nack a message with requeue=true in a loop and watch it come back forever until you wire up a DLX. Reading about a stale prefetch bug is nothing like watching your own consumer starve while a sibling consumer sits idle.

Across mock interviews run through LastRoundAI tagged backend, platform, or messaging, the prefetch and acknowledgment questions trip up more candidates than exchange routing does, even though exchange routing gets more study time by a wide margin. My guess is that exchange types feel like the "interesting" part to read about, while ack semantics feel like implementation detail right up until an interviewer asks why a queue's message count is climbing even though consumers are clearly running. I don't have a clean percentage to put on that pattern, only that it comes up often enough in review to flag here.

Get the reps in before the real thing

Reading an answer is not the same as defending it once an interviewer changes one variable on you, drops a node, doubles the prefetch, swaps a topic exchange for a headers exchange mid-conversation. LastRoundAI's mock interview mode runs backend and platform rounds with follow-up questions that adapt to what you actually said instead of a fixed script, and the free plan includes 15 credits a month that reset monthly rather than piling up. Starter is $19/mo if fifteen sessions a month isn't enough runway, and there's no native app to install, it runs in-browser on desktop or mobile.

If the slower part of the job hunt is finding enough backend or platform roles that actually mention a message broker, rather than passing the interview once you land one, Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

How long does it take to prepare for a RabbitMQ interview?

If you already work with RabbitMQ day to day, a focused week on the areas you avoid in practice is usually enough. Coming in cold, expect three to four weeks. The gap is rarely knowledge; it is being able to explain something you normally just use.

What RabbitMQ topics come up most often?

Interviewers concentrate on the parts that cause production incidents rather than the parts that are pleasant to learn. Expect the fundamentals to be assumed and the follow-up questions to sit one layer below what a tutorial covers.

Do I need hands-on RabbitMQ experience to pass?

It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.

Is RabbitMQ still worth learning in 2026?

For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

Leave a Reply

Your email address will not be published. Required fields are marked *