- My Development Notes/
- AMQP 0-9-1: The Complete Protocol/
- Consumer Acknowledgments and Delivery Guarantees/
Consumer Acknowledgments and Delivery Guarantees
Table of Contents
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-okwith the message (method, properties, body) if a message is available.basic.get-emptyif 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:
- Set
x-delivery-limiton a quorum queue (auto dead-letter after N deliveries). - Track delivery count in the
x-deathheaders and dead-letter after N attempts. - 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:
| Mode | When message is deleted | What happens on consumer crash |
|---|---|---|
At-most-once (auto_ack=True) | When broker delivers | Message lost (already deleted by broker) |
At-least-once (auto_ack=False, manual ack) | When consumer sends basic.ack | Message 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:
- The client:
channel.basic_cancel(consumer_tag)— graceful cancellation. - The broker: if the queue is deleted while a consumer is active, the broker sends
basic.cancelto 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 #
| Scenario | Result |
|---|---|
| Consumer processes and acks | Message deleted, at-least-once fulfilled |
| Consumer processes, crashes before ack | Message 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 crashes | Message lost |
| Channel closes (graceful or crash) | All unacked messages re-queued |
| Queue deleted while consumer active | consumer 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.