Consumer acknowledgements, nack and requeue

RabbitMQ · Practical · 12 min

Acks decide whether a crash loses messages or redelivers them; getting them wrong causes data loss or infinite loops.

With manual acknowledgements (auto_ack=False, the default in pika's basic_consume), the broker keeps every delivered message as unacked until the consumer responds:

  • basic_ack(delivery_tag): done, delete the message. multiple=True acknowledges all up to that tag.
  • basic_nack(delivery_tag, requeue=True): failed, put it back in the queue (at or near its original position) for redelivery.
  • basic_nack(..., requeue=False) or basic_reject: failed, discard it, or dead-letter it if the queue has a dead-letter exchange.

If the channel or connection closes with unacked messages, they are requeued automatically and redelivered with the redelivered flag set. This gives at-least-once processing: a message is never lost because a consumer crashed, but it may be processed twice.

Rules for robust consumers:

  • Ack after the work and its side effects are committed, never before.
  • Don't requeue forever on errors that will never succeed (malformed data): a poison message with requeue=True loops endlessly. Reject without requeue and dead-letter it. Quorum queues enforce a delivery limit for this.
  • Ack on the same channel the message arrived on; a delivery tag is per channel.
  • RabbitMQ closes channels that keep a message unacked longer than the consumer timeout (30 minutes by default). Very long tasks need a higher timeout or a different design.

Example

import json, pika

ch = pika.BlockingConnection(pika.ConnectionParameters("localhost")).channel()
ch.queue_declare(queue="emails", durable=True)
ch.basic_qos(prefetch_count=10)

def send_email(msg): print("sending to", msg["to"])

def on_message(channel, method, props, body):
    try:
        msg = json.loads(body)
    except ValueError:
        channel.basic_reject(method.delivery_tag, requeue=False)   # poison: drop/DLX
        return
    try:
        send_email(msg)
        channel.basic_ack(method.delivery_tag)                    # only after success
    except ConnectionError:
        channel.basic_nack(method.delivery_tag, requeue=True)     # transient: retry
    if method.redelivered:
        print("this message was delivered before")

ch.basic_consume(queue="emails", on_message_callback=on_message)  # manual acks
ch.start_consuming()

Remember

  • Ack after the work is done; unacked messages are redelivered if the channel dies
  • nack/reject with requeue=False drops or dead-letters poison messages
  • Manual acks give at-least-once delivery, so duplicates are possible

Watch out for

  • Requeueing a message that always fails, creating a hot redelivery loop.

Check yourself

  1. A consumer receives a message, writes to the database, then crashes before acking. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.rabbitmq.com/docs/confirms