Publisher Confirms
A plain publish is fire-and-forget — literally
Section titled “A plain publish is fire-and-forget — literally”Here is an uncomfortable fact about a basic publish: by default it returns nothing. Your client writes the message to the socket and moves on. If the broker was overloaded, the connection dropped at that instant, or the message was rejected, you never find out. The message is simply gone, and your code thinks it succeeded.
For a background analytics event, maybe that’s fine. For “the customer paid us”, it absolutely is not. Publisher confirms close this gap.
Publisher confirms: the broker acks your publish
Section titled “Publisher confirms: the broker acks your publish”When you put a channel into confirm mode, the broker sends back an acknowledgement for every message once it has taken responsibility for it (routed it to all matching durable queues, and — for persistent messages — written it to disk). You get an ack (the broker has it) or, rarely, a nack (the broker could not take it).
sequenceDiagram participant P as Publisher participant B as Broker P->>B: publish (confirm mode) B->>B: route + persist B-->>P: ack (I have it) Note over P: safe to consider it sent P->>B: publish (bad case) B-->>P: nack (could not accept) Note over P: retry or alert
Confirms are asynchronous: the broker streams acks back as it processes messages, so you don’t have to block after each publish. High-throughput publishers keep a window of unconfirmed messages in flight and only treat a message as durable once its ack arrives.
// amqplib: a confirm channel gives per-message callbacksconst channel = await conn.createConfirmChannel();
channel.publish('orders', 'order.created', Buffer.from(body), { persistent: true }, (err) => { if (err) console.error('NACK — message not confirmed, retry:', err); else console.log('ACK — broker has the message'); });# pika: enable confirms; publish raises/returns on failurechannel.confirm_delivery()
try: channel.basic_publish( exchange="orders", routing_key="order.created", body=body, properties=pika.BasicProperties(delivery_mode=2), # persistent mandatory=True, ) print("ACK — broker has the message")except pika.exceptions.UnroutableError: print("returned — no queue matched")except pika.exceptions.NackError: print("NACK — broker could not accept, retry")ch.Confirm(false) // put channel in confirm modeconfirms := ch.NotifyPublish(make(chan amqp.Confirmation, 1))
ch.PublishWithContext(ctx, "orders", "order.created", true, false, amqp.Publishing{ DeliveryMode: amqp.Persistent, Body: body,})
if c := <-confirms; c.Ack { log.Println("ACK — broker has the message")} else { log.Println("NACK — broker could not accept, retry")}The mandatory flag: catch unroutable messages
Section titled “The mandatory flag: catch unroutable messages”Confirms tell you the broker got the message — but a message routed to zero queues is still, from the broker’s view, handled successfully. It just vanishes. If you published order.created but no queue was bound for it (a typo, a missing binding), the message is dropped and you’d never know.
The mandatory flag fixes this: if a mandatory message can’t be routed to any queue, the broker returns it to the publisher instead of dropping it. Listen for returned messages and you’ll catch misrouting immediately.
Together, confirms + mandatory answer both questions: did the broker accept it? and did it actually reach a queue?
The trade-off: safety costs throughput
Section titled “The trade-off: safety costs throughput”Confirms aren’t free. Waiting for an ack after every single publish serializes your publisher and tanks throughput. The healthy middle ground:
- Async confirms with a window — keep N messages in flight, handle acks as they stream back. Near-full throughput, full safety.
- Batch confirms — publish a batch, wait for the batch to confirm. Simpler, slightly less throughput.
- Publish-and-wait per message — safest to reason about, slowest. Only for low-volume, critical publishes.