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

This section: Start With the Decision — A lost temperature sample and a repeated repair order do not cause the same harm.

A lost temperature sample and a repeated repair order do not cause the same harm. A broker confirm shows that a message was accepted for delivery; a consumer acknowledgement covers processing of a delivery. A separate application result shows whether the repair was done once.

9.2 Route Overview

This section: Route Overview — This is part 1 of 2.

This is part 1 of 2. Continue with AMQP Delivery: Sizing and Reliability Contracts.

9.3 Part Objectives

This section: 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 0-9-1 messages have properties (delivery mode, priority, expiration/TTL, correlation ID, message ID) and a body. Publisher confirms, persistence, routing, and consumer acknowledgements support at-least-once delivery across different links; they do not prove an exactly-once business result. The consumer must use a stable command identity and a durable result record to make repeated delivery safe. Dead-letter queues can retain selected rejected, expired, or overflowed messages for investigation when configured.

9.4 Learning Objectives

This section: 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 at-most-once and at-least-once delivery, and implement exactly-once processing with application deduplication 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

This section: 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

This section: 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 - improves recovery, but may arrive twice and still needs failure handling

Then: Exactly-once effect: Tracked mail where the receiver writes down each parcel number and ignores a second copy; the post office alone can’t promise only one delivery

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

This section: Message Structure — Start with the parcel before the delivery contract.

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

This section:

Broker Bex, Messaging and broker domain expertCheckpoint: 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

This section: Delivery Guarantees — Once the parcel is shaped, choose the promise: speed and low overhead, or stronger protection against loss.

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)

Each message is delivered one or more times: once the broker confirms it, it is retried rather than silently dropped, but duplicates are possible. This needs publisher confirms, durable queues and manual consumer ACKs. Delivery still depends on the configured broker and recovery path.

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 Processing (Effectively-Once)

AMQP 0-9-1 can redeliver after a lost acknowledgment. The aim here is one business effect even if the message is processed again.

Characteristics:

First: Uses a durable idempotency key at the business action

Next: Publisher confirms, durable storage, and manual acknowledgments protect other loss windows

Then: Test the crash window after the action but before the consumer acknowledgment

Illustrative implementation: The producer transaction concerns broker publishing only. The payment provider must support a durable idempotency key; the local Redis cache below is only a shortcut and cannot by itself prevent a repeat after a crash.

# Producer: Transactional publishing
channel.tx_select()
try:
    event_id = payment['event_id']  # Stable across publish retries
    channel.basic_publish(
        exchange='payments',
        routing_key='payment.process',
        body=json.dumps(payment),
        properties=pika.BasicProperties(delivery_mode=2, message_id=event_id)
    )
    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, idempotency_key=message_id)
        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 Bex, Messaging and broker domain expertCheckpoint: 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.
  • One business effect under redelivery needs a durable idempotency boundary; a broker transaction is not enough.
Try It: Delivery Guarantee Simulator

Select a delivery pattern in this simplified model. It assumes one retry after an uncertain first attempt; an unconfirmed result is counted as lost in the display. Real broker guarantees depend on confirms, persistence, routing, consumer acknowledgments, and recovery.


9.8 Publisher Confirms

This section: Publisher Confirms — Delivery promises begin before the consumer sees the message.

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: A publisher records ACK, NACK, or timeout after the broker reaches its configured acceptance boundary.

In Figure 9.3, start with a publish carrying a stable message ID. The broker then reaches the durable acceptance boundary configured for that connection before returning an ACK. A NACK or timeout leaves the producer without positive acceptance evidence; a timeout is ambiguous because the broker may already hold the first copy. Record that result and retry with the same ID so downstream processing can detect a duplicate. Finally, read the limit box: a confirm supports the claim that the broker accepted the publish, but it does not prove that a binding matched, a consumer finished, or a physical action occurred. The application must gather those later results separately.

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

This section: Consumer Acknowledgments — After broker receipt, ACK and NACK behavior decides whether a queued message is removed, retried, or routed elsewhere.

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: A consumer reports success with ACK or makes retry versus dead-letter handling explicit with NACK.

In Figure 9.4, follow one queued message into the consumer. If the work ends well, basic_ack() lets the broker remove that delivery. The ACK proves only that this attempt ended; it proves no later result. If the work fails, follow the NACK branch. With requeue=True, the broker can send the message again. The handler must therefore make a repeat safe. With requeue=False, the message moves toward the dead-letter queue for review. This fork ties the ACK rule to retry bounds, safe repeats, and prefetch limits. Each setting controls a different part of a failed attempt.

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 Bex, Messaging and broker domain expertCheckpoint: 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

This section: Dead Letter Queues — The last part of the reliability chain is evidence: failed messages should land somewhere inspectable.

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: A failed delivery leaves the main queue through the dead-letter exchange and waits in a DLQ for an operator decision.

In Figure 9.5, begin with a normal delivery from the main queue to the consumer. When processing fails, basic_nack(requeue=False) prevents an immediate retry loop and sends the broker toward the configured dead-letter exchange. The exchange routes the failed message into the DLQ, where it can be inspected with its failure context. Expiry and queue-length limits can also send work there; the configuration example’s 300000 ms / 1000 = 300 s, or 5 minutes, shows one TTL boundary. End at the operator decision. A DLQ preserves failed work, but recovery still requires a bounded choice to repair and replay, quarantine, or discard it.

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

This section: Continue to the Next Part — Carry this evidence into AMQP Delivery: Sizing and Reliability Contracts, which begins with Worked Example: Smart Factory Message Sizing.

Carry this evidence into AMQP Delivery: Sizing and Reliability Contracts, which begins with Worked Example: Smart Factory Message Sizing.

Find help

3 reviewed answers.

Where should I start in a module?

Open the module page from the sidebar to see its chapters in order. Use the module map to return to a topic you need.

Modules and paths

Reviewed

What can I try when a project does not work?

Check the symptom, then work through the diagnostic steps. Keep the error message and the smallest example that reproduces it.

When you are stuck

Reviewed

How do interactive examples work?

Use the interactive guide to check how examples and simulations work before changing your project.

Interactive examples guide

Reviewed