Skip to main content
  1. AMQP 0-9-1: The Complete Protocol/

Consumer Acknowledgments and Delivery Guarantees

Consumer Acknowledgments and Delivery Guarantees #

Delivery guarantees in AMQP are controlled by the acknowledgment model. The broker does not delete a message from the queue when it delivers it — it marks it unacknowledged (in-flight). The message is deleted only after the consumer explicitly acknowledges it. If the consumer dies without acknowledging, the broker re-delivers the message to another consumer.

This is AMQP’s at-least-once delivery model. Understanding the acknowledgment methods and their semantics is the core of building reliable consumers.

Two Consumption Models #

basic.consume (Push Delivery) #

The primary consumption model. The consumer registers with the broker via basic.consume; the broker pushes messages as they arrive.

def on_message(channel, method, properties, body):
    try:
        process(body)
        channel.basic_ack(delivery_tag=method.delivery_tag)
    except Exception:
        channel.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

channel.basic_consume(
    queue='orders',
    on_message_callback=on_message,
    auto_ack=False  # manual acknowledgment
)
channel.start_consuming()  # blocks, processing messages

Push semantics: the broker delivers messages to the consumer as fast as the channel’s prefetch limit allows (Chapter 11). The consumer does not poll — it receives callbacks (or async events) as messages arrive.

Consumer tag: basic.consume returns a consumer tag — a unique string identifying this subscription:

consumer_tag = channel.basic_consume(queue='orders', on_message_callback=on_message)
# consumer_tag: 'ctag1.abc123...' or user-defined

To cancel consumption: channel.basic_cancel(consumer_tag).

basic.get (Pull/Poll) #

One-shot message retrieval. The consumer explicitly requests one message at a time.

method, properties, body = channel.basic_get(queue='orders', auto_ack=False)
if method:  # None if queue is empty
    process(body)
    channel.basic_ack(delivery_tag=method.delivery_tag)

basic.get returns either:

  • basic.get-ok with the message (method, properties, body) if a message is available.
  • basic.get-empty if the queue is empty.

When to use basic.get: rarely. The push model (basic.consume) is almost always better for throughput. Use basic.get when:

  • Implementing a polling loop with application-controlled timing.
  • Building a CLI tool that processes one message and exits.
  • Checking queue status without subscribing.

Performance: basic.get is synchronous — each call is a round-trip. For high throughput, basic.consume with prefetch is orders of magnitude faster.

The Delivery Tag #

Every message delivered to a consumer has a delivery tag — a monotonically increasing 64-bit integer, scoped to the channel. The delivery tag is the handle used to acknowledge, nack, or reject the message.

Channel 1 delivery sequence:
  Message A → delivery_tag=1
  Message B → delivery_tag=2
  Message C → delivery_tag=3

channel.basic_ack(delivery_tag=2)  → acknowledges message B

Important: delivery tags are channel-local. The same message re-delivered on a new channel (after reconnect) gets a new delivery tag. Delivery tags from a closed channel cannot be used on a new channel.

Redelivered flag: when a message is re-delivered (after the original consumer died without acking), the basic.deliver method has redelivered=True. Consumers should check this flag for idempotency:

def on_message(channel, method, properties, body):
    if method.redelivered:
        # Check idempotency before processing
        if already_processed(properties.message_id):
            channel.basic_ack(delivery_tag=method.delivery_tag)
            return
    process(body)
    channel.basic_ack(delivery_tag=method.delivery_tag)

basic.ack — Acknowledging Messages #

basic.ack tells the broker the message was processed successfully. The broker deletes the message from the queue.

channel.basic_ack(
    delivery_tag=method.delivery_tag,
    multiple=False  # True = ack all messages up to and including this tag
)

multiple=True: acknowledges all messages with delivery tags up to and including the specified tag. Useful for batching acks:

messages = []
for i in range(100):
    method, properties, body = channel.basic_get('orders', auto_ack=False)
    messages.append((method.delivery_tag, body))

# Process all 100 messages, then bulk-ack
process_batch([body for _, body in messages])
channel.basic_ack(delivery_tag=messages[-1][0], multiple=True)
# Acknowledges delivery_tag=1 through 100 in one network round-trip

Bulk acks reduce network traffic for high-throughput consumers. The tradeoff: if the consumer crashes after processing messages 1–50 but before the bulk ack, all 100 messages are re-delivered.

basic.nack — Negative Acknowledgment (Bulk) #

basic.nack rejects a message (or batch of messages), optionally requeueing.

channel.basic_nack(
    delivery_tag=method.delivery_tag,
    multiple=False,  # True = nack all messages up to this tag
    requeue=True     # True = re-queue the message; False = discard or DLX
)

basic.nack is a RabbitMQ extension (not in the base AMQP 0-9-1 spec, but included in the 0-9-1 errata). It adds the multiple flag that basic.reject lacks.

requeue=True: the message is returned to the head of the queue (approximately — the broker may requeue to any position) and re-delivered to the next available consumer. Used when a transient error occurred and the message should be retried.

requeue=False: the message is discarded OR forwarded to the dead-letter exchange (if configured on the queue). Used when the message is permanently unprocessable (poison message, schema error, application bug).

The requeue loop trap: nacking with requeue=True on a message that always fails creates an infinite loop — the message is immediately re-delivered and nacked again, consuming CPU with no progress. Solutions:

  1. Set x-delivery-limit on a quorum queue (auto dead-letter after N deliveries).
  2. Track delivery count in the x-death headers and dead-letter after N attempts.
  3. Never requeue on permanent errors; always use requeue=False for unprocessable messages.

basic.reject — Single Message Rejection #

basic.reject rejects a single message. It predates basic.nack and lacks the multiple flag.

channel.basic_reject(
    delivery_tag=method.delivery_tag,
    requeue=False  # discard/DLX; True = requeue
)

basic.reject and basic.nack(multiple=False) are functionally equivalent for single-message operations.

Auto-Acknowledgment Mode #

Setting auto_ack=True in basic.consume or basic.get disables manual acknowledgment. The broker considers the message acknowledged the moment it is delivered — before the consumer processes it.

channel.basic_consume(
    queue='logs',
    on_message_callback=on_message,
    auto_ack=True  # fire-and-forget delivery
)

Auto-ack semantics: at-most-once delivery. If the consumer crashes after receiving but before processing, the message is lost — the broker already deleted it.

When to use auto-ack:

  • Logging pipelines where some message loss is acceptable.
  • Read-only consumers (monitoring, analytics) where duplicate processing is worse than loss.
  • Low-value data where reprocessing is expensive.

Performance: auto-ack is faster than manual ack (no ack frame round-trip). For throughput-optimized consumers that can tolerate loss, auto-ack + high prefetch is the fastest AMQP delivery model.

At-Most-Once vs At-Least-Once #

AMQP provides two delivery guarantees through the ack model:

ModeWhen message is deletedWhat happens on consumer crash
At-most-once (auto_ack=True)When broker deliversMessage lost (already deleted by broker)
At-least-once (auto_ack=False, manual ack)When consumer sends basic.ackMessage re-delivered to another consumer

AMQP does not natively provide exactly-once delivery. The application must make consumers idempotent (using message-id or application-level deduplication) to achieve effectively-once semantics at the application layer.

Exactly-once = at-least-once delivery + idempotent consumer

What Happens to Unacknowledged Messages #

When a consumer’s channel or connection closes without acknowledging messages, those messages transition back to the ready state in the queue. They are re-delivered to the next available consumer.

Timeline:
  t=0: Broker delivers msg to consumer A (delivery_tag=5, status=unacked)
  t=1: Consumer A crashes (channel closes)
  t=2: Broker detects close → msg returns to 'ready' state
  t=3: Broker delivers msg to consumer B (delivery_tag=1, redelivered=True)

The re-delivery is transparent to the queue — no message is created or destroyed. The broker simply moves the message from the unacked set back to the ready set.

Unacked message visibility: unacknowledged messages are visible in the broker’s management API as “unacked” count. A large unacked count with a slow consumer is a sign of consumer latency, not a queue depth problem.

Consumer Cancellation #

A consumer subscription can be cancelled by:

  1. The client: channel.basic_cancel(consumer_tag) — graceful cancellation.
  2. The broker: if the queue is deleted while a consumer is active, the broker sends basic.cancel to the consumer.
def on_cancel(method):
    print(f"Consumer {method.consumer_tag} was cancelled by broker")
    # Reconnect or exit

channel.add_on_cancel_callback(on_cancel)

After receiving basic.cancel, the consumer should re-subscribe (if the queue was recreated) or reconnect. Unacked messages from a cancelled consumer are re-queued.

Delivery Guarantee Summary #

ScenarioResult
Consumer processes and acksMessage deleted, at-least-once fulfilled
Consumer processes, crashes before ackMessage re-delivered (duplicate processing risk)
Consumer rejects (nack/reject, requeue=False)Message dead-lettered or discarded
Consumer rejects (nack/reject, requeue=True)Message re-queued, re-delivered
auto_ack=True, consumer crashesMessage lost
Channel closes (graceful or crash)All unacked messages re-queued
Queue deleted while consumer activeconsumer receives basic.cancel, messages gone

Summary #

AMQP delivery semantics are controlled by acknowledgment. basic.ack commits processing; basic.nack/basic.reject rejects with optional requeue. Manual ack provides at-least-once delivery; auto-ack provides at-most-once. The delivery tag is the channel-local handle for acking. Messages unacked when a channel closes are automatically re-queued. Exactly-once requires idempotent consumers on top of at-least-once delivery — AMQP does not provide it natively.