Publisher Confirms in RabbitMQ: The Complete Guide to Guaranteed Message Delivery

Publisher confirms are the only way to guarantee RabbitMQ received your message. This guide covers how they work, the performance tradeoffs at different batch sizes, and production-ready code examples in Python, Node.js, and PHP.

DanubeData Engineering

10 min read

2,916 views

rabbitmqpublisher-confirmsguaranteed-deliveryamqpmessage-queueperformance

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:

  1. The producer enables confirm mode on the channel
  2. The producer publishes a message
  3. The broker writes the message to disk (for persistent messages) or memory
  4. The broker sends back a basic.ack to confirm receipt
  5. 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 SizeThroughputConfirm LatencyThroughput vs. No Confirms
No confirms123,296 msg/s—100%
10,000111,554 msg/s73 ms90%
5,000108,260 msg/s42 ms88%
3,00068,271 msg/s40 ms55%
1,00018,564 msg/s52 ms15%
2006,181 msg/s31 ms5%

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.

Browse all articles
Engineering11 min read

A Server Lost Power at 00:32. We Found Out at 08:18.

One machine stopped, and S3, the container registry and every deployment stopped with it for eight hours. Our monitoring caught it in three minutes and told nobody. Here is the full postmortem: why single-machine redundancy was not what we thought it was, the near-miss we nearly caused ourselves, and the pager and hardware watchdog we shipped the same day.

Adrian Silaghi1,229 views
Engineering12 min read

Incident review — S3 object storage instability, April 30 2026

On April 30 2026, our S3 object storage service experienced intermittent disruptions over a six-hour window. A faulty 10 Gigabit network card on a newly added storage server triggered repeated kernel watchdog reboots. Here is what happened, what our diagnosis got wrong on multiple occasions, and what we are changing as a result.

Adrian Silaghi1,016 views

Ready to get started?

Deploy your infrastructure in minutes with DanubeData's managed services.