When you call basic_publish in RabbitMQ, the message is written to a TCP buffer and sent to the broker. But "sent" does not mean "received." If the connection drops mid-transfer, the broker runs out of memory, or the queue does not exist, your message disappears silently.
Publisher confirms are the mechanism that turns "sent" into "guaranteed delivered." When enabled, the broker sends an acknowledgment back to the producer for every message (or batch of messages) it successfully persisted. Without confirms, you are running fire-and-forget — and hoping for the best.
How Publisher Confirms Work
The flow is simple:
- The producer enables confirm mode on the channel
- The producer publishes a message
- The broker writes the message to disk (for persistent messages) or memory
- The broker sends back a
basic.ackto confirm receipt - If the broker cannot accept the message, it sends a
basic.nack
The key insight is that confirms are asynchronous — the broker does not confirm each message individually in real-time. Instead, it batches confirmations, which is why the --confirm parameter in benchmarks controls how many messages you wait to confirm at once.
The Performance vs. Safety Tradeoff
We benchmarked different confirm batch sizes on our managed RabbitMQ service using the official rabbitmq-perf-test tool. The results reveal a clear tradeoff:
| Confirm Batch Size | Throughput | Confirm Latency | Throughput vs. No Confirms |
|---|---|---|---|
| No confirms | 123,296 msg/s | — | 100% |
| 10,000 | 111,554 msg/s | 73 ms | 90% |
| 5,000 | 108,260 msg/s | 42 ms | 88% |
| 3,000 | 68,271 msg/s | 40 ms | 55% |
| 1,000 | 18,564 msg/s | 52 ms | 15% |
| 200 | 6,181 msg/s | 31 ms | 5% |
The sweet spot is a batch size of 5,000: you retain 88% of fire-and-forget throughput while getting a broker acknowledgment for every message. Below 3,000, throughput drops sharply because the producer spends most of its time waiting for the confirm round-trip.
Why Small Batches Are Slow
Each waitForConfirms call incurs a network round-trip. If your broker is 15ms away, a batch size of 200 means you can only send about 200 / 0.015 = 13,333 confirms per second — and each confirm covers 200 messages, giving you a theoretical max of ~13K msg/s. Our measured 6,181 msg/s includes serialization overhead and TCP buffering delays.
At batch size 5,000, the same math gives 5000 / 0.015 = 333,333 theoretical max, well above what a single queue can handle. The confirm overhead becomes negligible.
Code Examples
Python (pika)
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(
host='amqp-myqueue-tenant.queues.danubedata.ro',
port=5672,
credentials=pika.PlainCredentials('admin', 'YOUR_PASSWORD')
)
)
channel = connection.channel()
channel.queue_declare(queue='orders', durable=True)
# Enable publisher confirms
channel.confirm_delivery()
# Every publish now blocks until the broker confirms
try:
channel.basic_publish(
exchange='',
routing_key='orders',
body=b'{"order_id": 42}',
properties=pika.BasicProperties(delivery_mode=2), # persistent
mandatory=True,
)
print("Message confirmed by broker")
except pika.exceptions.UnroutableError:
print("Message could not be routed to a queue")
except pika.exceptions.NackError:
print("Broker rejected the message")
Node.js (amqplib) — Batched Confirms
import amqplib from 'amqplib';
const conn = await amqplib.connect(
'amqp://admin:YOUR_PASSWORD@amqp-myqueue-tenant.queues.danubedata.ro'
);
const ch = await conn.createConfirmChannel();
await ch.assertQueue('orders', { durable: true });
const BATCH_SIZE = 5000;
let pending = 0;
for (const order of orders) {
const ok = ch.publish('', 'orders',
Buffer.from(JSON.stringify(order)),
{ persistent: true }
);
if (!ok) {
// Channel buffer full — wait for drain
await new Promise(resolve => ch.once('drain', resolve));
}
pending++;
// Flush confirms every BATCH_SIZE messages
if (pending >= BATCH_SIZE) {
await ch.waitForConfirms(); // throws if any were nacked
pending = 0;
}
}
// Flush remaining
if (pending > 0) {
await ch.waitForConfirms();
}
await conn.close();
PHP (php-amqplib)
<?php
use PhpAmqpLibConnectionAMQPStreamConnection;
use PhpAmqpLibMessageAMQPMessage;
$connection = new AMQPStreamConnection(
'amqp-myqueue-tenant.queues.danubedata.ro',
5672, 'admin', 'YOUR_PASSWORD'
);
$channel = $connection->channel();
$channel->queue_declare('orders', false, true, false, false);
// Enable publisher confirms
$channel->confirm_select();
// Publish with confirmation
$msg = new AMQPMessage(
json_encode(['order_id' => 42]),
['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]
);
$channel->basic_publish($msg, '', 'orders');
// Wait for broker confirmation
$channel->wait_for_pending_acks_returns(5.0); // 5 second timeout
$channel->close();
$connection->close();
When to Use Publisher Confirms
Always Use Confirms For:
- Payment processing: A lost payment event means a customer gets charged without receiving their product
- Order pipelines: A dropped order message means a sale that never gets fulfilled
- Transactional email: Password resets, invoices, and account verifications cannot be silently lost
- Audit logs: Compliance requires proof that every event was recorded
- Data synchronization: Missing sync events cause data drift between systems
Confirms Are Optional For:
- Metrics and telemetry: A few dropped data points do not affect dashboards
- Real-time notifications: Missing one push notification is recoverable
- Cache invalidation: A missed invalidation is self-correcting on the next TTL expiry
Common Mistakes
1. Confirming Every Single Message
The worst pattern is calling waitForConfirms after every publish. This turns your producer into a synchronous, one-at-a-time pipeline bottlenecked by network latency. Use batched confirms with a batch size of 3,000-5,000 for the best throughput.
2. Ignoring Nacks
A basic.nack means the broker explicitly rejected your message — usually because the queue does not exist, a policy limit was exceeded, or internal storage failed. Always handle nacks: log them, retry with backoff, or route to a dead-letter queue.
3. Confusing Confirms with Consumer Acks
Publisher confirms guarantee the broker received the message. Consumer acknowledgments guarantee the consumer processed the message. You need both for end-to-end delivery guarantees.
4. Using Transactions Instead
AMQP transactions (tx.select, tx.commit) provide similar guarantees but are 10-100x slower than publisher confirms. They were the original mechanism before confirms were introduced. Always prefer confirms.
Conclusion
Publisher confirms cost you only 12% throughput (108K vs 123K msg/s at batch size 5,000) while guaranteeing every message reaches the broker. For any workload where losing a message has business impact, that is the best trade you will ever make.
All benchmarks were run on DanubeData's managed RabbitMQ using the official rabbitmq-perf-test tool. Deploy your own instance and verify these numbers yourself.