Chapters

9 AMQP Delivery: Guarantees and Acknowledgements

amqp
arch
messages
delivery
reliability
dlq

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.
In 60 Seconds

Choose Delivery Proof From the Harm of a Miss

Picture a factory service sending both a routine temperature reading and a repair order. Losing one reading may be acceptable. Losing the order is not. Repeating the order may also create two visits, so the sender needs evidence of the receiver’s final result, not only evidence that a message left.

AMQP is a set of messaging rules for moving labelled messages through a message service. For each message class, record identity, sender, receiver, allowed age, repeat rule, order need, storage need, acceptance point, and final business result.

Stop the receiver before and after it accepts work. Lose the reply, restart the message service, fill its waiting space, reject an invalid message, and restore service. Count sends, accepts, repeats, results, and parked failures. Give physical or paid actions their own identity so a repeated message cannot silently repeat the outcome.

Stored delivery does not prove that the receiver acted, and a confirmation does not prove that the message was true. The application must close its own result.

Practitioner builds the delivery contract and failure test. Under the Hood explains message parts, acknowledgments, persistence, confirmation windows, dead-letter handling, and why exactly-once wording needs a stated boundary.

Use this delivery trace for each class:

  • Give each real job an identity.
  • State how old it may be.
  • State if order must hold.
  • State if repeats are safe.
  • Count each send and accept.
  • Count each final business result.
  • Stop the receiver before acceptance.
  • Stop it just after acceptance.
  • Lose the reply and retry.
  • Inspect work parked for review.
  • Reconcile all counts after recovery.
  • Keep the failed case as a test.
  • Name who owns parked work.
  • Set a time to review it.
  • Mark old work before replay.
  • Close each real job once. 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.

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:

Deep Dives:

Related Concepts:

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:

TermSimple Explanation
PersistentMessage saved to disk, survives broker restart
ACKConsumer says “I processed this message successfully”
NACKConsumer says “I failed to process this, please retry”
Dead Letter QueueSpecial queue for messages that can’t be processed
Publisher ConfirmBroker 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!”

Chapter Roadmap
  • 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.

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 9.1: AMQP message structure with header, properties, and body sections

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:

FieldDescriptionCommon Values
DurableMessage survives broker restartTrue (persistent), False (transient)
PriorityMessage priority level0-9 (0 lowest, 9 highest)
TTLTime-To-Live before expirationMilliseconds (e.g., 60000 for 1 minute)
First-AcquirerFirst consumer to receiveBoolean
Delivery-CountNumber of delivery attemptsInteger

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:

PropertyDescriptionExample
Content-TypeMIME type of bodyapplication/json, text/plain
Content-EncodingEncoding appliedgzip, utf-8
Correlation-IDLinks related messagesUUID for request-reply matching
Reply-ToReturn address queuereply_queue_abc123
Message-IDUnique identifierUUID for deduplication
TimestampCreation timeUnix timestamp
TypeApplication-specific typesensor.reading, order.created
App-IDProducing applicationtemperature-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

Broker BexCheckpoint: 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.

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.


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.

Comparison of at-most-once delivery with possible loss, at-least-once delivery with acknowledgment, retry, and possible duplicates, and exactly-once intent requiring transaction or deduplication. A caveat states that broker or transport guarantees do not prove that a business effect happened exactly once; consumers still need idempotency and completion evidence.
Figure 9.2: AMQP delivery guarantee levels: at-most-once, at-least-once, exactly-once

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

Broker BexCheckpoint: 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.
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.


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.

Publisher confirm sequence in which a producer publishes with a stable message ID, the broker reaches its configured durable acceptance boundary, and the broker returns ACK, NACK, or timeout evidence. The producer records the outcome and retries with the same ID; the confirm proves broker acceptance, not consumer processing or physical action.
Figure 9.3: Publisher confirm sequence: publish, persist, and broker acknowledgment

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.

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 9.4: Consumer acknowledgment flow with success ACK and failure NACK paths

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

TypeMethodEffect
Positive ACKbasic_ack()Message removed from queue
Negative ACKbasic_nack()Message requeued or sent to DLQ
Rejectbasic_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 DurationRecommended PrefetchReason
< 100 ms50-100Reduce round-trip overhead
100 ms - 1s10-20Balance throughput and fairness
1s - 10s1-5Prevent consumer overload
> 10s1One task at a time
Broker BexCheckpoint: Acknowledgments and Prefetch

You now know:

  • A positive ACK removes the message; a NACK can requeue it or dead-letter it.
  • prefetch_count=1 is 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.
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.


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.

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 9.5: Dead letter queue pattern for handling failed messages

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


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.