9 AMQP Delivery: Guarantees and Acknowledgements
Start with the story: An AMQP message is a parcel with a label, a body, and a delivery receipt path. The chapter is about choosing when that parcel may be dropped, retried, acknowledged, or parked for investigation.
9.1 Start With the Decision
A lost temperature sample and a repeated repair order do not cause the same harm. Delivery proof must fit each message.
9.2 Route Overview
This is part 1 of 2. Continue with AMQP Delivery: Sizing and Reliability Contracts.
9.3 Part Objectives
- Choose at-most-once, at-least-once, or stronger handling.
- Connect publisher confirms, acknowledgements, and dead letters.
9.4 Learning Objectives
By the end of this chapter, you will be able to:
- Analyze Message Structure: Distinguish the role of AMQP header, properties, and body sections and explain how each field affects message routing and delivery
- Configure Delivery Modes: Select appropriate message persistence, priority, and time-to-live (TTL) settings to match application reliability requirements
- Implement Delivery Guarantees: Evaluate and implement at-most-once, at-least-once, and exactly-once semantics based on data criticality and overhead trade-offs
- Apply Acknowledgments: Demonstrate correct use of publisher confirms and consumer acknowledgments to construct reliable messaging pipelines
- Handle Message Failures: Design dead-letter queue configurations and justify negative acknowledgment strategies for production IoT systems
Key Concepts
First: Message Properties: AMQP metadata: delivery-mode (persistent/transient), priority, expiration, content-type, correlation-id
Next: Delivery Mode: 1 = transient (memory only, lost on restart), 2 = persistent (disk-written, survives restart)
Then: Acknowledgment Mode: Auto-ack removes messages immediately on delivery; manual-ack waits for explicit consumer confirmation
After that: Message Priority: 0-9 scale for priority queues — higher priority messages are delivered first to consumers
Also inspect: Content Type: MIME type header (e.g., application/json, application/cbor) enabling consumer-side deserialization
Finally: Correlation ID: Identifier linking RPC request to reply message — essential for request/reply patterns over queues
Finally: Redelivery Flag: Set on messages redelivered after consumer crash — enables idempotent handling of at-least-once delivery
9.5 Prerequisites
Before diving into this chapter, you should be familiar with:
- AMQP Core Architecture: This chapter builds on understanding of exchanges, queues, bindings, and the producer-broker-consumer model
- AMQP Fundamentals: Core AMQP concepts and protocol basics
- Networking Basics: TCP connections and reliable delivery concepts
Deep Dives:
- AMQP Core Architecture - Exchanges, queues, and bindings
- AMQP Frames and Reliability - Advanced features and protocol frames
- AMQP Implementations and Labs - Hands-on broker setup
Related Concepts:
- MQTT QoS Levels - Compare with MQTT delivery guarantees
- CoAP Reliability - RESTful reliability patterns
Think of AMQP message delivery like sending registered mail:
First: At-Most-Once: Regular mail - might get lost, but fastest and cheapest
Next: At-Least-Once: Tracked mail with retry - guaranteed to arrive, might arrive twice if tracking fails
Then: Exactly-Once: Registered mail with signature - guaranteed exactly one delivery, most expensive
Key terms:
| Term | Simple Explanation |
|---|---|
| Persistent | Message saved to disk, survives broker restart |
| ACK | Consumer says “I processed this message successfully” |
| NACK | Consumer says “I failed to process this, please retry” |
| Dead Letter Queue | Special queue for messages that can’t be processed |
| Publisher Confirm | Broker tells producer “I received your message” |
“My temperature alert says the warehouse is overheating, but the cooling system never turned on!” Temperature Terry cried. “What happened to my message?”
the microcontroller pulled up the message logs. “Let’s check. When you sent your alert, did you ask for a publisher confirm?” Sammy looked blank. “That’s like asking the post office to text you when they receive your package. Without it, you just toss the message and hope for the best.”
“So what should I do?” asked Sammy. “Use at-least-once delivery,” said the LED. “The broker will keep your message and send back an ACK — an acknowledgment — when the cooling system processes it. If the cooling system crashes before saying ACK, the broker sends the message again. Your alert won’t get lost!”
“But what if a message is truly undeliverable?” the battery asked. “Then it goes to the dead letter queue,” Max explained. “Think of it as the ‘lost and found’ box. Engineers can check it later to find out what went wrong. No message just vanishes into thin air!”
- In 60 Seconds
- Key Concepts
- Prerequisites
- Related Chapters
- For Beginners: Message Delivery
- The Tracked Package
- Message Structure
- Quick Check: RPC over Queues
- Checkpoint: Message Anatomy
- Try It: AMQP Message Builder
- Delivery Guarantees
- Checkpoint: Delivery Promise
- Quick Check: Choosing the Right Delivery Mode
- Try It: Delivery Guarantee Simulator
- Publisher Confirms
- Quick Check: Synchronous vs. Asynchronous Confirms
- Consumer Acknowledgments
- Checkpoint: Acknowledgments and Prefetch
- Try It: Prefetch & Consumer Distribution
- Dead Letter Queues
- Quick Check: What Is (and Isn’t) a DLQ Trigger
- Try It: Dead Letter Queue Scenario Explorer
9.6 Message Structure
Start with the parcel before the delivery contract. Reliability depends on the routing, persistence, timing, and identity metadata the message carries.
An AMQP message consists of multiple sections that provide metadata and payload.
9.6.1 AMQP Message Format
Inspect Figure 9.1 before assigning application fields so transport controls and business payload do not become mixed together.
Read Figure 9.1 from header to properties to body. Delivery controls such as durability and priority belong in the header; identifiers and content description travel as properties; the application value remains in the body. That separation supports routing, correlation, and payload evolution without redefining the whole envelope.
9.6.2 Header Section
The header contains delivery-related metadata:
| Field | Description | Common Values |
|---|---|---|
| Durable | Message survives broker restart | True (persistent), False (transient) |
| Priority | Message priority level | 0-9 (0 lowest, 9 highest) |
| TTL | Time-To-Live before expiration | Milliseconds (e.g., 60000 for 1 minute) |
| First-Acquirer | First consumer to receive | Boolean |
| Delivery-Count | Number of delivery attempts | Integer |
Example: Setting persistent, high-priority message:
import pika
properties = pika.BasicProperties(
delivery_mode=2, # Persistent (survives restart)
priority=8, # High priority (0-9 scale)
expiration='60000' # TTL: 60 seconds
)
channel.basic_publish(
exchange='orders',
routing_key='order.priority',
body='{"order_id": 12345}',
properties=properties
)
9.6.3 Properties Section
Properties provide message metadata for routing and processing:
| Property | Description | Example |
|---|---|---|
| Content-Type | MIME type of body | application/json, text/plain |
| Content-Encoding | Encoding applied | gzip, utf-8 |
| Correlation-ID | Links related messages | UUID for request-reply matching |
| Reply-To | Return address queue | reply_queue_abc123 |
| Message-ID | Unique identifier | UUID for deduplication |
| Timestamp | Creation time | Unix timestamp |
| Type | Application-specific type | sensor.reading, order.created |
| App-ID | Producing application | temperature-sensor-01 |
Example: Complete message with properties:
properties = pika.BasicProperties(
content_type='application/json',
content_encoding='utf-8',
message_id=str(uuid.uuid4()),
correlation_id=request_correlation_id,
reply_to='response_queue',
timestamp=int(time.time()),
type='sensor.temperature',
app_id='weather-station-01',
delivery_mode=2
)
9.6.4 Body Section
The body contains the actual message payload:
First: Can be binary or text
Next: Encoding specified in properties
Then: No size limit (but broker may impose limits)
After that: Common formats: JSON, Protocol Buffers, MessagePack
Checkpoint: Message Anatomy
You now know:
- The header holds durable, priority, TTL, first-acquirer, and delivery-count.
- Properties carry content-type, correlation-id, reply-to, message-id, timestamp, type, and app-id.
- The body can be binary or text, and its format should match the consumer’s content-type handling.
9.7 Delivery Guarantees
Once the parcel is shaped, choose the promise: speed and low overhead, or stronger protection against loss.
AMQP provides configurable delivery semantics to match application requirements.
9.7.1 Three Delivery Modes
Before selecting a delivery mode, inspect the loss, duplicate, and coordination boundaries in Figure 9.2.
Read Figure 9.2 from at-most-once to at-least-once and then the exactly-once intent. Acknowledgment and retry trade possible loss for possible duplication; stronger coordination costs more state. The final caveat returns the design to application idempotency because transport evidence alone cannot prove a business effect happened exactly once.
9.7.2 1. At-Most-Once (0 or 1)
Message delivered once or not at all. Fastest but may lose messages.
Characteristics:
First: No acknowledgment required
Next: Fastest, lowest overhead
Then: Fire-and-forget pattern
Implementation:
# Producer: No confirms
channel.basic_publish(
exchange='sensors',
routing_key='temperature',
body='22.5',
properties=pika.BasicProperties(delivery_mode=1) # Transient
)
# Consumer: Auto-acknowledge
channel.basic_consume(
queue='sensor_data',
on_message_callback=callback,
auto_ack=True # Message removed immediately on delivery
)
Use cases:
After that: Non-critical telemetry
Also inspect: Sensor readings where occasional loss is acceptable
Finally: High-throughput streaming where speed matters more than completeness
9.7.3 2. At-Least-Once (1 or more)
Message guaranteed to be delivered, but may arrive multiple times.
Characteristics:
First: Requires acknowledgment
Next: Retries on failure
Then: Possible duplicates (consumer must handle)
Implementation:
# Producer: With confirms
channel.confirm_delivery()
try:
channel.basic_publish(
exchange='orders',
routing_key='order.created',
body=json.dumps(order),
properties=pika.BasicProperties(
delivery_mode=2, # Persistent
message_id=str(uuid.uuid4())
)
)
except pika.exceptions.UnroutableError:
# Handle unroutable message
retry_or_log(order)
# Consumer: Manual acknowledgment
def callback(ch, method, properties, body):
try:
process_order(body)
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception:
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
channel.basic_consume(
queue='orders',
on_message_callback=callback,
auto_ack=False # Manual ACK required
)
Use cases:
After that: Important events and commands
Also inspect: Order processing (with idempotent handlers)
Finally: Notifications that must be delivered
9.7.4 3. Exactly-Once (exactly 1)
Message delivered exactly once with no loss or duplicates.
Characteristics:
First: Uses transactions or deduplication
Next: Highest reliability, highest overhead
Then: Most complex to implement
Implementation (transaction-based):
# Producer: Transactional publishing
channel.tx_select()
try:
channel.basic_publish(
exchange='payments',
routing_key='payment.process',
body=json.dumps(payment),
properties=pika.BasicProperties(delivery_mode=2)
)
channel.tx_commit()
except Exception:
channel.tx_rollback()
raise
# Consumer: With deduplication
def callback(ch, method, properties, body):
message_id = properties.message_id
# Check if already processed
if redis.exists(f'processed:{message_id}'):
ch.basic_ack(delivery_tag=method.delivery_tag)
return
try:
result = process_payment(body)
redis.setex(f'processed:{message_id}', 86400, '1') # 24h TTL
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception:
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
Use cases:
After that: Financial transactions
Also inspect: Critical commands that cannot be duplicated
Finally: Billing and accounting events
Checkpoint: Delivery Promise
You now know:
- At-most-once is fastest because it uses no acknowledgment, but messages may be lost.
- At-least-once adds ACKs and retries, so consumers need duplicate handling such as message-ID.
- Exactly-once uses transactions or deduplication when duplicates are worse than the overhead.
9.8 Publisher Confirms
Delivery promises begin before the consumer sees the message. A publisher confirm answers: did the broker safely receive it?
Publisher confirms allow producers to know when messages are safely received by the broker.
Inspect Figure 9.3 to mark the precise promise a producer receives before adding retries to a publish loop.
Read Figure 9.3 from publish and broker persistence to ACK, NACK, or timeout. A retry should reuse a stable message identifier because an absent confirm leaves acceptance ambiguous. The sequence proves broker responsibility at its configured boundary, not routing to a queue or completion by a consumer.
Implementation:
# Enable publisher confirms
channel.confirm_delivery()
# Synchronous confirm (blocking)
channel.basic_publish(
exchange='orders',
routing_key='order.new',
body=order_json,
mandatory=True
)
# Raises exception if not confirmed
# Asynchronous confirms (non-blocking)
def on_confirm(frame):
if frame.method.NAME == 'Basic.Ack':
print(f"Message {frame.method.delivery_tag} confirmed")
else:
print(f"Message {frame.method.delivery_tag} rejected")
channel.add_on_return_callback(on_confirm)
9.9 Consumer Acknowledgments
After broker receipt, ACK and NACK behavior decides whether a queued message is removed, retried, or routed elsewhere.
Consumer acknowledgments ensure messages are processed successfully before removal from queues.
Inspect Figure 9.4 to locate the point where consumer processing becomes broker-visible evidence.
Follow Figure 9.4 from delivery into processing, then compare the success ACK with the failure NACK branches. An ACK permits removal; a NACK must explicitly choose requeue or dead-letter handling. This decision is why prefetch and idempotency belong beside acknowledgment policy rather than being tuned independently.
9.9.1 ACK Types
| Type | Method | Effect |
|---|---|---|
| Positive ACK | basic_ack() | Message removed from queue |
| Negative ACK | basic_nack() | Message requeued or sent to DLQ |
| Reject | basic_reject() | Single message reject (legacy) |
9.9.2 Prefetch Count
Prefetch controls how many unacknowledged messages a consumer can hold.
Read prefetch as a bounded in-flight window, not a universal speed setting. Start with handler duration and round-trip overhead, then check fairness across consumers and the redelivery burst after a crash. Raise the value only when measured idle time shows that additional overlap improves throughput without hiding too much unfinished work in one process.
# Limit to 1 unacknowledged message at a time
channel.basic_qos(prefetch_count=1)
# Best for:
# - Long-running tasks (prevents one consumer hoarding all work)
# - Fair distribution across multiple consumers
Prefetch recommendations:
| Task Duration | Recommended Prefetch | Reason |
|---|---|---|
| < 100 ms | 50-100 | Reduce round-trip overhead |
| 100 ms - 1s | 10-20 | Balance throughput and fairness |
| 1s - 10s | 1-5 | Prevent consumer overload |
| > 10s | 1 | One task at a time |
Checkpoint: Acknowledgments and Prefetch
You now know:
- A positive ACK removes the message; a NACK can requeue it or dead-letter it.
prefetch_count=1is fair for long-running tasks because one consumer cannot hoard work.- Faster work can use 50-100 under 100 ms, 10-20 for 100 ms to 1 s, and 1-5 for 1 s to 10 s.
9.10 Dead Letter Queues
The last part of the reliability chain is evidence: failed messages should land somewhere inspectable.
Dead letter queues (DLQ) capture messages that cannot be processed successfully.
Inspect Figure 9.5 to see how a failed delivery leaves the normal work path without losing the message’s diagnostic value.
Trace Figure 9.5 from main-queue delivery through processing failure and requeue=false, then follow broker routing through the dead-letter exchange into the DLQ. The final queue is a holding and evidence boundary, not proof of recovery; operators still need a bounded retry or repair decision.
9.10.1 DLQ Triggers
Messages are sent to DLQ when:
First: Consumer rejects with requeue=false: basic_nack(requeue=False)
Next: Message TTL expires: Message exceeded time-to-live
Then: Queue length exceeded: Queue reached maximum length
After that: Message rejected after max retries: Application-level retry exhaustion
9.10.2 DLQ Configuration
# Declare dead letter exchange
channel.exchange_declare(
exchange='dlx',
exchange_type='direct'
)
# Declare dead letter queue
channel.queue_declare(queue='dead_letters')
channel.queue_bind(queue='dead_letters', exchange='dlx', routing_key='failed')
# Declare main queue with DLQ configuration
channel.queue_declare(
queue='orders',
arguments={
'x-dead-letter-exchange': 'dlx',
'x-dead-letter-routing-key': 'failed',
'x-message-ttl': 300000 # Optional: 5 min TTL
}
)
9.11 Continue to the Next Part
Carry this evidence into AMQP Delivery: Sizing and Reliability Contracts, which begins with Worked Example: Smart Factory Message Sizing.
