Publisher Confirms in RabbitMQ: A Receipt, Not a Delivery Guarantee
Publisher confirms tell you the broker accepted a message — not that it reached a queue or a consumer. Here's the design that closes the gap.
24 Mar 2026, 02:31 UTC

A publisher calls basicPublish, the client returns without an exception, the connection stays healthy — and the order never appears downstream. That silent gap is why teams enable publisher confirms. It is also why confirms alone often fail to close it.
The useful framing: a confirm is a receipt from the broker saying it has taken responsibility for the message. It says nothing about whether the message reached a queue, nothing about whether a consumer processed it, and nothing about duplicates. Confirms are worth turning on, but they belong to a three-part design — confirm handling, unroutable-message handling, and idempotent consumers.
What a confirm actually promises
Calling confirmSelect() puts a channel into confirm mode. From then on, every publish on that channel gets a monotonically increasing sequence number, and the broker asynchronously sends basic.ack or basic.nack keyed to that number. Confirm mode is per channel, so a second channel publishing to the same exchange is unaffected unless you enable it there too.
The scope of the promise is broker-side acceptance. For a persistent message on a durable queue, that generally includes writing it to disk, and on replicated queue types it generally includes replication to followers — but the exact durability boundary depends on the queue type and the broker version you run. Check the publisher-confirms documentation for your version rather than assuming.
Synchronous versus asynchronous confirmation
The Java client offers waitForConfirms() and waitForConfirmsOrDie(). Both block the channel until outstanding publishes are acknowledged; the second throws on a nack. This is easy to reason about, but it serializes your publisher: publish, wait for a round trip, publish again. Per-message latency becomes dominated by network round trips rather than by broker work.
The asynchronous path uses a confirm listener with separate ack and nack callbacks, plus a map from sequence number to your own message identifier. You keep publishing and let callbacks retire entries. Keep that map bounded — an unbounded outstanding-publish map is a memory leak waiting for a slow broker. Also note the callbacks run on a client library thread, so avoid blocking work inside them.
The gap confirms leave open: unroutable messages
If a routing key matches no binding, the broker can accept the message and drop it — and you still get an ack. Confirms cannot detect this, because from the broker's perspective nothing went wrong.
Setting the mandatory flag on basicPublish changes that: the broker returns the message via basic.return instead of discarding it, and a return listener on the channel receives it. An alternate exchange is the other common way to catch unroutable traffic. Either way, a confirm-only design is incomplete without one of them.
A worked shape
The following is illustrative Java-client code, not a tested program. Method names differ in Spring AMQP, the .NET client, and Go libraries — match the API of the library you actually use.
Channel ch = connection.createChannel();
ch.confirmSelect(); // channel now in confirm mode
ch.addConfirmListener(this::onAck, this::onNack);
ch.addReturnListener((replyCode, replyText, exchange,
routingKey, props, body) -> {
// unroutable: props.getMessageId() identifies the message
scheduleRetry(props.getMessageId());
});
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.messageId(UUID.randomUUID().toString()) // stable id for dedupe
.deliveryMode(2) // persistent
.build();
long seq = ch.getNextPublishSeqNo(); // read BEFORE publishing
outstanding.put(seq, props.getMessageId());
ch.basicPublish("orders", "order.created", true /* mandatory */,
props, body);
The pieces that matter: a unique message id stamped on every publish, a sequence-to-id map populated before the publish call, a return listener for unroutable messages, and a retry path with exponential backoff for nacks and returns. On the consumer side, dedupe on that message id — a unique constraint, an upsert, or an inbox table — because the retry path will eventually deliver something twice.
Where this still breaks
- Duplicates are not eliminated. A nack or a timeout followed by a republish produces two copies. Confirms give at-least-once; end-to-end exactly-once is not on offer.
- A nack is not always a routing failure. On replicated queue types, losing leadership before replication completes can produce a nack. Treat nacks as retryable with backoff, not as permanent errors, and add jitter so a broker incident does not turn into a republish storm.
- Transactions are the wrong tool here.
txSelect/txCommitgive atomic publish batches but are substantially slower than confirms and are not the standard reliability path. - Behaviour varies by version and client. Confirm defaults, queue-type interactions, and callback threading differ across broker releases and libraries.
How to check it on your own broker
- Declare a durable queue, confirm-select a channel, and publish a batch while logging ack and nack callbacks together with their sequence numbers. Confirm the numbers you recorded before publishing match the ones you receive.
- Publish with
mandatoryset to a routing key with no binding and confirm abasic.returnreaches your return listener. - Restart the broker or disrupt a replicated queue leader, then observe which publishes are nacked and how your retry logic behaves.
- Publish the same message id twice and confirm the consumer applies the effect once.
- Measure synchronous versus asynchronous confirm throughput on your own hardware before quoting any number — persistence settings, message size, and batching dominate the result.
If those five checks pass, you have a publisher that knows what the broker accepted, knows what it dropped, and can survive a retry without corrupting state. That is the realistic ceiling for publisher-side reliability in RabbitMQ.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.