AMQP’s acknowledgment mechanism is a lie, at least if you think it means your message is guaranteed to be processed by your consumer.

Let’s see it in action. We’ll use rabbitmq and python-amqp for this.

First, set up a simple producer and consumer.

Producer (producer.py):

import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='my_queue')

message_body = b'Hello, AMQP!'
channel.basic_publish(exchange='', routing_key='my_queue', body=message_body)
print(f" [x] Sent '{message_body.decode()}'")

connection.close()

Consumer (consumer.py):

import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='my_queue')

def callback(ch, method, properties, body):
    print(f" [x] Received '{body.decode()}'")
    # Simulate some processing time
    time.sleep(5)
    # This is where the acknowledgment happens
    ch.basic_ack(delivery_tag=method.delivery_tag)
    print(" [x] Acknowledged")

channel.basic_consume(queue='my_queue',
                      on_message_callback=callback,
                      auto_ack=False) # Crucially, we'll ack manually

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

Run the producer, then the consumer. You’ll see the message sent and received. The consumer will then pause for 5 seconds before printing "Acknowledged". If you stop the consumer during those 5 seconds, the message will be redelivered. This is the core of what people think AMQP guarantees.

The problem is, basic_ack only tells the broker "I have received this message and am now responsible for it." It doesn’t tell the broker "I have successfully processed this message."

Consider this scenario:

  1. Producer sends message M.
  2. Broker delivers M to Consumer A.
  3. Consumer A receives M.
  4. Consumer A sends basic_ack to the Broker.
  5. Broker marks M as delivered and acknowledged.
  6. Consumer A begins processing M (e.g., writing to a database).
  7. Consumer A crashes before the database write is complete.

From the broker’s perspective, M was successfully delivered and acknowledged. The broker has no idea that the consumer failed after acknowledging it. The message is gone, and its processing is incomplete. This is where the "guarantee" breaks.

The fundamental levers you control are the auto_ack setting and the timing of your basic_ack call within the consumer’s callback. Setting auto_ack=True (which is the default if you don’t specify False) means the broker considers the message acknowledged as soon as it’s delivered to the consumer. This is the fastest but least reliable option. By setting auto_ack=False and calling basic_ack after your processing logic, you gain the ability to control when the broker considers the message handled.

To achieve true processing guarantees, you need to implement idempotent consumers and handle potential failures after the basic_ack call. This often involves transactionality at the application level or using dead-letter queues to capture messages that have been redelivered too many times, indicating persistent processing failures. The broker’s acknowledgment is a signal of reception and responsibility transfer, not successful execution.

What people often miss is that basic_nack and basic_reject are also powerful tools. If your consumer receives a message and immediately knows it cannot process it (e.g., malformed data, invalid state), it can basic_nack or basic_reject with the requeue=True flag. This tells the broker to return the message to the queue for another consumer to attempt, or even the same consumer if it was a transient issue. However, this still doesn’t solve the problem of a crash after a successful basic_ack.

The next logical step is to explore how to use basic_nack and basic_reject with dead-lettering to manage unprocessable messages.

Want structured learning?

Take the full Amqp course →