Message Acknowledgments & Redelivery
Learn message acknowledgments and redelivery in queues. Understand how ack/nack works, redelivery semantics, and best practices. Hands-on walkthrough included.
Focus: message acknowledgments and redelivery
You've built a queue, pushed messages, and written consumers that process them — but what happens when a worker crashes halfway through a job, or a message makes it to your consumer but the database write fails? If you don't handle acknowledgments and redelivery properly, you'll face lost messages, duplicate processing, and silent data corruption. This lesson demystifies the acknowledgment dance between producers, brokers, and consumers, and gives you the mental model and hands-on skills to build resilient messaging pipelines that never lose a message and can safely handle duplicates.
The problem this lesson solves
Without explicit acknowledgments, a message broker faces an impossible choice: it can't know if a consumer actually processed a message or died before finishing. If the broker marks a message as delivered and forgets about it, a crashed worker means the message is permanently lost. If it never marks anything as delivered, every message is redelivered forever, and your consumers would process the same item hundreds of times.
The real-world pain is immediate:
- Lost orders: A payment service crashes after debiting a customer but before sending the confirmation email.
- Duplicate charges: A retry mechanism redelivers a message because the ack was slow, and your system charges the customer twice.
- Infinite loops: A consumer that always fails but never nacks pushes a poison message back into the queue endlessly.
You need a clear protocol that tells the broker: "I got this message, I'm working on it, and here's the outcome." That protocol is the acknowledgment — and its counterpart, redelivery, is what happens when the ack never arrives.
By the end of this lesson, you'll understand how acknowledgments and redelivery work under the hood, how to use them in Python with a real broker (RabbitMQ), and how to design your consumers so that duplicates are harmless.
Core concept / mental model
Think of a message broker as a responsible delivery driver. When you hand a package to the driver, they don't just drop it at the first open door — they wait for a signature. That signature is the acknowledgment. If no one signs, the driver keeps the package and tries again later — that's redelivery.
In messaging terms:
- Producer sends a message to a queue.
- Consumer fetches the message and processes it.
- Acknowledgment (ack) tells the broker: "Message processed successfully, you can remove it from the queue."
- Negative acknowledgment (nack) tells the broker: "Something went wrong, requeue or dead-letter this message."
- Redelivery happens when a consumer fails to ack (connection drops, timeout, or explicit reject with requeue).
The key mental shift: delivery is not processing. A message has only been truly consumed once the broker receives an ack. Until then, the broker considers it still owned by that consumer, but if the consumer disappears, the message becomes eligible for redelivery to another consumer.
Different brokers use different terminology:
- RabbitMQ: uses
basic_ack,basic_nack, andbasic_reject. - Redis Streams: uses
XACKandXCLAIMwith a pending entries list. - Apache Kafka: uses consumer offsets (commit) instead of per-message acks.
Despite the names, the principle is universal: explicit confirmation prevents loss and enables exactly-once (or at-least-once) processing.
How it works step by step
When a consumer pulls a message from a queue, here's what happens behind the scenes:
- Consumer subscribes to a queue with
auto_ack=False(or equivalent). - Broker delivers a message, tagging it as "unacked" for that consumer.
- Consumer processes the message — performs business logic, writes to DB, calls external APIs.
- Consumer sends ack if processing succeeded. The broker removes the message from the queue.
- On failure, the consumer either:
- Sends a nack with
requeue=True— the broker puts the message back in the queue (possibly at the head) for redelivery. - Sends a nack withrequeue=False— the broker routes the message to a dead-letter queue (DLQ) for later inspection. - If the consumer crashes without acking, the broker waits for a timeout (or detects the closed connection) and requeues the message automatically.
The critical detail: a redelivered message retains a redelivered flag so your consumer can distinguish first delivery from retries. This flag is your friend — use it to log, count, or special-case.
Hands-on walkthrough
Let's put theory into practice with RabbitMQ and the pika Python library. First, install the dependency:
pip install pika
Scenario: A payment service with manual acks
We'll build a consumer that processes payment messages and manually acks or nacks.
import pika
import json
def process_payment(ch, method, properties, body):
data = json.loads(body)
print(f"Processing payment {data['order_id']}")
try:
# Simulate a business operation that could fail
if data.get('amount', 0) <= 0:
raise ValueError("Invalid amount")
# ... do the actual charge here ...
print(f"Payment {data['order_id']} succeeded")
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception as e:
print(f"Payment {data['order_id']} failed: {e}")
# Requeue the message for retry, but only if it hasn't been retried too many times
if method.redelivered:
print("Already redelivered once — sending to DLQ")
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
else:
print("Requeuing for one more attempt")
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='payments')
channel.basic_consume(queue='payments', on_message_callback=process_payment, auto_ack=False)
print('Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
Expected output (when you publish a valid and an invalid message):
Processing payment 1001
Payment 1001 succeeded
Processing payment 1002
Payment 1002 failed: Invalid amount
Requeuing for one more attempt
Processing payment 1002
Payment 1002 failed: Invalid amount
Already redelivered once — sending to DLQ
Publishing with confirmation
Production-grade publishing also uses acknowledgments — publisher confirms ensure the broker has accepted your message.
import pika
import json
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.confirm_delivery()
message = json.dumps({'order_id': 1001, 'amount': 49.99})
try:
channel.basic_publish(exchange='', routing_key='payments', body=message)
print("Message confirmed by broker")
except pika.exceptions.UnroutableError:
print("Message could not be routed")
Simulating a crash
To see automatic redelivery in action, write a consumer that processes a message and then sleeps without acking, while a second consumer is active. The broker will redeliver after the connection closes.
# consumer_crash.py — same setup, but after receiving one message, raises an exception
def on_message(ch, method, properties, body):
print("Got message, will crash now")
raise RuntimeError("Worker died")
# auto_ack=False and no ack is sent
When you run this worker and then publish a message, the broker will redeliver it to another consumer or requeue it after the channel closes.
Compare options / when to choose what
| Ack mode | How it works | Best for | Risks |
|---|---|---|---|
| Auto-ack | Broker marks message as delivered immediately on send | Fire-and-forget, low-value logs | Message loss if consumer crashes before processing |
| Manual ack | Consumer sends ack after processing | Critical workflows (payments, orders) | None — but requires discipline |
| Nack with requeue | Requeue for immediate redelivery | Transient errors (network blips) | Infinite loop if error is permanent |
| Nack without requeue (DLQ) | Send to dead-letter queue | Permanent failures after retries | Must monitor DLQ |
| Publisher confirms | Producer waits for broker ack | Guaranteed delivery from producer side | Slight latency |
When to choose what:
- Use manual ack for any system where data loss is unacceptable.
- Use nack with requeue only when the error is likely transient (e.g., database lock).
- Use DLQ for poison messages that will never succeed — combined with a retry counter.
- Use auto-ack only for metrics/telemetry where losing a sample is fine.
Troubleshooting & edge cases
"Message is processed twice even though I acked"
- Cause: The ack was sent after your business logic, but the broker hadn't received it before the connection died; or you acked inside a transaction that rolled back.
- Fix: Always ack after the side effect is committed. If you write to a database, commit first, then ack. This at-least-once pattern will cause duplicates — handle them with idempotency keys.
"My consumer gets stuck in an infinite redelivery loop"
- Cause: You nack with
requeue=Trueevery time, even for permanent errors. - Fix: Track
method.redeliveredor use a delivery count header. After N attempts, nack withrequeue=Falseto DLQ.
"Messages disappear after my consumer crashes"
- Cause: Using
auto_ack=True(default in many clients) — the broker removes the message before processing. - Fix: Set
auto_ack=Falseand ack manually.
"Redelivery takes forever"
- Cause: The consumer connection is still open but the consumer is unresponsive (e.g., blocked on a network call). The broker won't requeue until the channel closes.
- Fix: Set a consumer timeout (RabbitMQ:
consumer_timeoutin ms) or implement a heartbeat that detects lost connections.
What you learned & what's next
You now understand the core of message acknowledgment: messages are only truly consumed when you ack them. You've seen how nack with and without requeue works, how redelivery affects your processing logic, and how to avoid common pitfalls like duplicates and infinite loops. You can confidently build consumers that use manual acks and publisher confirms for reliable, at-least-once delivery.
Next up: In the next lesson, you'll tackle idempotency — the essential companion to redelivery. Because redelivery means duplicates, you'll learn how to design your services to process the same message multiple times without side effects. That's the secret to true resilience in distributed systems.
Pro tip: Always assume a message will be redelivered at least once. Build your idempotency strategy before you deploy your first ack-based consumer.
Now go practice: implement a consumer that uses manual acks and a redelivery counter, and watch what happens when you simulate a crash.
Practice recap
In your next hands-on session, extend the payment consumer from this lesson to include a retry counter: maintain a dictionary of delivery counts and nack with requeue=False after three attempts. Publish a malformed message and observe how it ends up in the dead-letter queue. Then create a second queue that consumes from the DLQ and logs the poison message for manual inspection.
Common mistakes
- Using auto_ack=True in production — messages are deleted before processing, so a crash = data loss.
- Acking before the side effect completes — e.g., ack then write to DB; if the DB fails, the message is lost.
- Nacking with requeue=True for permanent errors, causing infinite redelivery loops.
- Not handling the redelivered flag or a delivery counter, so poisoned messages retry forever and clog the queue.
- Forgetting to enable publisher confirms, so producers never know if the broker accepted their message.
Variations
- Kafka uses consumer offset commits instead of per-message acks — redelivery happens on rebalance or offset expiration.
- Redis Streams: use XACK and XCLAIM for manual acknowledgment and claiming of stuck pending entries.
- SQS has a visibility timeout: if you don't delete the message, it becomes visible again for redelivery.
Real-world use cases
- Order processing system: credit card charges are acked only after the payment gateway confirms success.
- Email notification service: resends emails if the ack times out, relying on idempotency tokens to avoid duplicates.
- Video transcoding pipeline: a worker acks after the output file is written to object storage, ensuring no lost renders.
Key takeaways
- Acknowledgment tells the broker a message was successfully processed; without it, the broker redelivers the message.
- Manual acks (auto_ack=False) prevent data loss but introduce the possibility of duplicates — design for at-least-once.
- Use nack with requeue for transient failures and nack without requeue (or DLQ) for permanent errors.
- The redelivered flag or delivery count helps you implement retry limits and detect poison messages.
- Publisher confirms give producers confidence that messages reached the broker.
- Idempotency is a necessary companion to redelivery — build it into your consumers.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.