66  AMQP Messages & Delivery

In 60 Seconds

AMQP messages consist of headers, properties (delivery mode, priority, TTL, correlation ID), and a body payload. The protocol supports three delivery guarantee levels – at-most-once, at-least-once, and exactly-once – implemented through publisher confirms and consumer acknowledgments. Dead-letter queues capture undeliverable or rejected messages for later investigation, preventing silent data loss.

66.1 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

  • Message Properties: AMQP metadata: delivery-mode (persistent/transient), priority, expiration, content-type, correlation-id
  • Delivery Mode: 1 = transient (memory only, lost on restart), 2 = persistent (disk-written, survives restart)
  • Acknowledgment Mode: Auto-ack removes messages immediately on delivery; manual-ack waits for explicit consumer confirmation
  • Message Priority: 0-9 scale for priority queues — higher priority messages are delivered first to consumers
  • Content Type: MIME type header (e.g., application/json, application/cbor) enabling consumer-side deserialization
  • Correlation ID: Identifier linking RPC request to reply message — essential for request/reply patterns over queues
  • Redelivery Flag: Set on messages redelivered after consumer crash — enables idempotent handling of at-least-once delivery

66.2 Prerequisites

Before diving into this chapter, you should be familiar with:

Deep Dives:

Related Concepts:

Think of AMQP message delivery like sending registered mail:

  • At-Most-Once: Regular mail - might get lost, but fastest and cheapest
  • At-Least-Once: Tracked mail with retry - guaranteed to arrive, might arrive twice if tracking fails
  • 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!” Sammy the Sensor cried. “What happened to my message?”

Max 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 Lila 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?” Bella 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!”


66.3 Message Structure

An AMQP message consists of multiple sections that provide metadata and payload.

66.3.1 AMQP Message Format

Diagram showing AMQP message structure with three sections: header containing delivery metadata like durable, priority, and TTL; properties section with content-type, correlation-ID, and message-ID; and body section containing the payload
Figure 66.1: AMQP message structure with header, properties, and body sections

66.3.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
)

66.3.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
)

66.3.4 Body Section

The body contains the actual message payload:

  • Can be binary or text
  • Encoding specified in properties
  • No size limit (but broker may impose limits)
  • Common formats: JSON, Protocol Buffers, MessagePack
Try It: AMQP Message Builder

Construct an AMQP message by selecting header fields, properties, and a payload format. See how each choice affects the total message size and structure.


66.4 Delivery Guarantees

AMQP provides configurable delivery semantics to match application requirements.

66.4.1 Three Delivery Modes

Diagram comparing three AMQP delivery guarantee levels: at-most-once delivery with no acknowledgment (fastest, may lose messages), at-least-once delivery with acknowledgment and retry (guaranteed delivery, may duplicate), and exactly-once delivery with transaction or deduplication (guaranteed exactly one delivery, highest overhead)
Figure 66.2: AMQP delivery guarantee levels: at-most-once, at-least-once, exactly-once

66.4.2 1. At-Most-Once (0 or 1)

Message delivered once or not at all. Fastest but may lose messages.

Characteristics:

  • No acknowledgment required
  • Fastest, lowest overhead
  • 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:

  • Non-critical telemetry
  • Sensor readings where occasional loss is acceptable
  • High-throughput streaming where speed matters more than completeness

66.4.3 2. At-Least-Once (1 or more)

Message guaranteed to be delivered, but may arrive multiple times.

Characteristics:

  • Requires acknowledgment
  • Retries on failure
  • 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:

  • Important events and commands
  • Order processing (with idempotent handlers)
  • Notifications that must be delivered

66.4.4 3. Exactly-Once (exactly 1)

Message delivered exactly once with no loss or duplicates.

Characteristics:

  • Uses transactions or deduplication
  • Highest reliability, highest overhead
  • 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:

  • Financial transactions
  • Critical commands that cannot be duplicated
  • Billing and accounting events

Try It: Delivery Guarantee Simulator

Select a delivery guarantee level and simulate sending messages to see how each mode handles success, failure, and network issues. Adjust the failure rate to observe retries, duplicates, and lost messages.


66.5 Publisher Confirms

Publisher confirms allow producers to know when messages are safely received by the broker.

Sequence diagram showing publisher confirm flow: producer publishes message to broker, broker persists message to disk, broker sends acknowledgment back to producer confirming safe receipt
Figure 66.3: Publisher confirm sequence: publish, persist, and broker acknowledgment

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)

66.6 Consumer Acknowledgments

Consumer acknowledgments ensure messages are processed successfully before removal from queues.

Flowchart showing consumer acknowledgment paths: broker delivers message to consumer, consumer processes message, if successful consumer sends ACK and message is removed from queue, if failed consumer sends NACK and message is requeued or sent to dead-letter queue
Figure 66.4: Consumer acknowledgment flow with success ACK and failure NACK paths

66.6.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)

66.6.2 Prefetch Count

Prefetch controls how many unacknowledged messages a consumer can hold.

# 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
< 100ms 50-100 Reduce round-trip overhead
100ms - 1s 10-20 Balance throughput and fairness
1s - 10s 1-5 Prevent consumer overload
> 10s 1 One task at a time
Try It: Prefetch & Consumer Distribution

Adjust the prefetch count and number of consumers to see how messages are distributed. Observe how different prefetch values affect fairness and throughput when consumers have varying processing speeds.


66.7 Dead Letter Queues

Dead letter queues (DLQ) capture messages that cannot be processed successfully.

Sequence diagram showing dead-letter queue (DLQ) flow: consumer receives message from main queue, processing fails, consumer sends NACK with requeue=false, broker routes message to dead-letter exchange, message lands in DLQ for later investigation
Figure 66.5: Dead letter queue pattern for handling failed messages

66.7.1 DLQ Triggers

Messages are sent to DLQ when:

  1. Consumer rejects with requeue=false: basic_nack(requeue=False)
  2. Message TTL expires: Message exceeded time-to-live
  3. Queue length exceeded: Queue reached maximum length
  4. Message rejected after max retries: Application-level retry exhaustion

66.7.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
    }
)
Try It: Dead Letter Queue Scenario Explorer

Configure a queue with DLQ settings and simulate message processing. Watch how messages flow from the main queue to the dead letter queue under different failure conditions.


66.8 Worked Example: Smart Factory Message Sizing

Scenario: A pharmaceutical manufacturing plant monitors 500 machines via AMQP. Each machine publishes three types of messages with different delivery requirements:

Message Type Frequency Payload Delivery Need
Temperature telemetry Every 5 seconds 120 bytes JSON Occasional loss OK
Quality alert On threshold breach (~10/hour/machine) 350 bytes JSON Must not be lost
Batch completion event ~2 per hour per machine 800 bytes JSON Exactly once (audit trail)

Step 1: Calculate message rates and bandwidth

  • Telemetry: 500 machines x 1 msg/5s = 100 msg/s
  • Quality alerts: 500 x 10/hr = 5,000/hr = 1.39 msg/s
  • Batch events: 500 x 2/hr = 1,000/hr = 0.28 msg/s
  • Total: ~102 msg/s

Step 2: Calculate per-message AMQP overhead

Each AMQP message includes: - Frame header: 8 bytes - Method frame (basic.publish): ~40 bytes (exchange name, routing key) - Content header: ~60 bytes (properties: delivery_mode, content_type, timestamp, message_id, correlation_id) - Content body frame: 8 bytes header + payload

Total overhead per message: ~116 bytes

Step 3: Choose delivery mode for each type

Type QoS Overhead per msg Rationale
Telemetry At-most-once 120 + 116 = 236 B Next reading in 5s replaces any loss
Quality alert At-least-once 350 + 116 = 466 B + ACK (40 B) Must arrive; dedup via message_id
Batch event Exactly-once 800 + 116 = 916 B + TX overhead (~200 B) Audit requires no duplicates

Step 4: Aggregate bandwidth

  • Telemetry: 100 msg/s x 236 B = 23,600 B/s = 189 Kbps
  • Alerts: 1.39 msg/s x 506 B = 703 B/s = 5.6 Kbps
  • Batch: 0.28 msg/s x 1,116 B = 312 B/s = 2.5 Kbps
  • Total: ~197 Kbps (well within a 1 Mbps LAN connection)

AMQP protocol overhead per message consists of multiple frame components:

\[ \text{Overhead}_{\text{AMQP}} = \text{frame}_{\text{header}} + \text{method}_{\text{frame}} + \text{content}_{\text{header}} + \text{body}_{\text{frame}} \]

\[ = 8B + 40B + 60B + 8B = 116 \text{ bytes minimum} \]

Message efficiency for different payload sizes:

\[ \text{Efficiency} = \frac{\text{payload}}{\text{payload} + \text{overhead}} \times 100\% \]

Payload Total Size Efficiency
10 B 126 B 7.9% (poor)
120 B 236 B 50.8% (moderate)
800 B 916 B 87.3% (good)

For telemetry with 120-byte payloads, nearly half the bandwidth is protocol overhead. Batching 10 readings into one 1,200-byte message improves efficiency from 51% to 91%, saving \((10 \times 236B) - (1,200B + 116B) = 1,044B\) per 10 readings (44% reduction).

66.8.1 Interactive Calculator: AMQP Message Sizing

66.8.2 Interactive Calculator: AMQP Bandwidth Requirements

Step 5: Broker memory for persistent sessions

Quality alerts and batch events use persistent delivery (delivery_mode=2): - Persistent messages per second: 1.39 + 0.28 = 1.67 msg/s - Average persistent message size: ~810 bytes (weighted: (1.39 x 506 + 0.28 x 1116) / 1.67 ≈ 608 B on-wire; ~810 B with broker internal framing) - If a consumer goes offline for 10 minutes: 1.67 x 600 = 1,002 messages ≈ ~810 KB queued - With 5 consumers, worst case: ~4 MB broker memory for queued messages

Conclusion: The telemetry (98% of messages) uses fire-and-forget, keeping broker load minimal. Only the 2% of messages that matter (alerts + batch events) use persistent delivery, consuming modest broker resources. Using exactly-once for all messages would increase bandwidth to ~360 Kbps (vs. 197 Kbps) – roughly 1.8x – due to adding ~200 bytes of transaction overhead per telemetry message. That overhead is waste for data that is replaced every 5 seconds.

66.9 Knowledge Check

Test your understanding of message structure and delivery guarantees.

66.10 Summary

This chapter covered AMQP message structure and delivery guarantees:

  • Message Format: Analyzed header (durable, priority, TTL), properties (content-type, correlation-ID, message-ID), and body sections
  • Delivery Modes: Compared at-most-once (fast, may lose), at-least-once (guaranteed, may duplicate), and exactly-once (no loss, no duplicates)
  • Publisher Confirms: Implemented broker acknowledgment to producers for reliable publishing
  • Consumer Acknowledgments: Applied manual ACK/NACK with prefetch control for processing guarantees
  • Dead Letter Queues: Configured DLQ for capturing failed messages for investigation
  • Idempotency: Used message-ID for deduplication in at-least-once scenarios

66.11 What’s Next

Chapter Focus Why Read It
AMQP Features and Frames SASL authentication, TLS encryption, and AMQP 1.0 frame types Secure and inspect the protocol layer that carries the messages you configured here
AMQP Core Architecture Exchanges, queues, bindings, and the producer-broker-consumer model Reinforce how message routing decisions upstream affect the delivery guarantees you set
AMQP Implementations and Labs RabbitMQ broker setup, hands-on queue and DLQ configuration Apply publisher confirms and DLQ patterns in a running broker environment
MQTT QoS Levels MQTT at-most-once, at-least-once, and exactly-once equivalents Compare AMQP delivery semantics with the constrained-device protocol used across most IoT edge devices
CoAP Fundamentals and Architecture RESTful CoAP reliability via CON/NON message types Contrast AMQP’s broker-mediated guarantees with CoAP’s direct endpoint reliability model
Protocol Integration Patterns Bridging AMQP with MQTT, CoAP, and HTTP in IoT gateways See how the delivery guarantees studied here propagate (or break) across protocol translation boundaries