Persist Messages with Delivery Mode 2
Persist messages with delivery mode 2 — Messaging & queues tutorial, lesson 7. Learn how to make your messages durable, ensure they survive broker restarts, and apply this in a hands-on exercise.
Focus: persist messages with delivery mode 2
You've built a producer, a consumer, and a queue that moves messages around nicely — until the broker restarts and every unacknowledged message vanishes into thin air. That's the exact pain point this lesson solves: how to make messages survive crashes and reboots using delivery mode 2 (also called persistent delivery). If you're running anything more than a toy demo, losing messages on restart is the kind of bug that gets you paged at 3 a.m. — so let's fix it properly.
The problem this lesson solves
Imagine you've just deployed a payment processing service. Your producer sends a "charge the customer" message to RabbitMQ, and the broker holds it until your consumer picks it up. Everything looks perfect — until the broker's host runs out of memory and restarts. When it comes back, that message is gone. No error, no trace, just a silent loss of critical data. That's the default behavior: delivery mode 1 (transient) keeps messages only in memory, so any broker restart wipes them out.
The lesson's pain is real and urgent: in production, message loss is often worse than downtime. A queue that seems to work in dev can fail catastrophically under restart conditions. You need a way to tell the broker, "This message matters — write it to disk before you acknowledge it." That's what delivery mode 2 does. It's the difference between a message that lives only in RAM and one that's safely stored and can be redelivered after a crash.
By the end of this lesson, you'll know how to persist messages with delivery mode 2, why it's essential for reliability, and how to apply it in a practical exercise. You'll also understand when persistent delivery is the right call and when it's overkill.
Core concept / mental model
Think of the broker as a post office. In transient mode (delivery mode 1), the post office holds your letters in a clerk's hands — fast to process, but if the clerk drops them, they're gone. In persistent mode (delivery mode 2), the clerk writes your letter into a ledger before promising delivery. The ledger lives on disk, so even if the post office burns down, the letter is recoverable later.
In RabbitMQ terms, a message marked with delivery_mode: 2 is written to the queue's durable storage before the broker sends an acknowledgment back to the producer. This ensures that the message survives broker restarts and can be redelivered if the consumer hasn't acked it yet. The key players are:
- Producer — the client that sends the message.
- Broker (RabbitMQ) — the intermediary that stores and forwards messages.
- Queue durability — must be declared with
durable=Trueso the queue itself survives a restart. - Delivery mode 2 — the per-message flag that tells the broker to persist that specific message.
Pro tip: Delivery mode 2 only works if the queue is also durable. A persistent message in a non-durable queue is like a letter in a ledger for a warehouse that's scheduled for demolition — the ledger survives, but the building doesn't.
A common misconception is that delivery mode 2 alone makes a message permanent. In reality, it's a two-part contract: the queue must be durable (declared with durable=True) and the message must be sent with delivery_mode=2. If either is missing, messages can still be lost on broker restart.
How it works step by step
The flow for persisting a message with delivery mode 2 is straightforward, but each step matters:
1. Declare a durable queue
The producer and consumer must declare the same queue with durable=True. This tells RabbitMQ to store the queue definition and its messages on disk. If you don't do this, the queue exists only in memory and will be deleted on restart — regardless of delivery mode.
2. Publish with delivery mode 2
When publishing, you set the delivery_mode property to 2. Most client libraries (like pika for Python) expose this as BasicProperties(delivery_mode=2). This marks the message as persistent.
3. Broker writes to disk (and confirms)
Upon receiving the message, the broker writes it to disk, then sends a publish confirmation back to the producer (if publisher confirms are enabled). This is a performance cost, but it's the price for durability.
4. Consumer receives and acknowledges
The consumer receives the message and, after processing it, sends an acknowledgment (basic_ack). Until that ack is received, the broker considers the message unprocessed.
5. Broker restart recovery
If the broker restarts before ack, it reloads the persistent message from disk and can redeliver it to the same or another consumer. Without delivery mode 2, the message is gone.
Hands-on walkthrough
Let's build a small RabbitMQ example in Python using pika. You'll need a running RabbitMQ server (Docker works great). We'll create a durable queue, publish a persistent message, verify it's still there after restart, and consume it.
Setup
First, install pika if you haven't already:
pip install pika
For a quick RabbitMQ server, use Docker:
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
Producer with delivery mode 2
Here's a complete producer that sends a message with delivery_mode=2:
import pika
# Connect to RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# Declare a durable queue
channel.queue_declare(queue='persistent_queue', durable=True)
# Publish a message with delivery mode 2
message = "payment: user_123, amount: 100.00"
channel.basic_publish(
exchange='',
routing_key='persistent_queue',
body=message,
properties=pika.BasicProperties(
delivery_mode=2, # Persistent delivery
),
)
print(f" [x] Sent '{message}' with delivery mode 2")
connection.close()
Expected output: [x] Sent 'payment: user_123, amount: 100.00' with delivery mode 2
Consumer with acknowledgment
Now a consumer that processes the message and acks it:
import pika
import time
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# Declare the same durable queue
channel.queue_declare(queue='persistent_queue', durable=True)
def callback(ch, method, properties, body):
print(f" [x] Received {body.decode()}")
time.sleep(1) # Simulate work
ch.basic_ack(delivery_tag=method.delivery_tag)
print(" [x] Done and acknowledged")
channel.basic_consume(queue='persistent_queue', on_message_callback=callback)
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()
Run the producer, then the consumer. After the consumer processes, the message is gone from the queue.
Verify persistence across restart
To see durability in action:
- Send a few messages with delivery mode 2 (but don't run the consumer).
- Stop the RabbitMQ container:
docker stop rabbitmq - Start it again:
docker start rabbitmq - Check the queue — the messages should still be unacked and redeliverable.
You can inspect the queue with the management UI at http://localhost:15672 (guest/guest). You'll see the queue has messages waiting, proving they survived the restart.
Compare options / when to choose what
Delivery mode 2 is the default choice for critical messages, but it isn't the only option. Here's a comparison:
| Aspect | Delivery mode 1 (transient) | Delivery mode 2 (persistent) |
|---|---|---|
| Storage | Memory only | Disk + memory |
| Survives broker restart? | No | Yes |
| Performance | Faster (no disk I/O) | Slower (disk write per message) |
| Use cases | Logs, metrics, ephemeral data | Orders, payments, events that must not lose |
| Queue durability required? | Not necessarily | Yes (durable=True) |
When to choose what?
- Use delivery mode 2 for any message where loss is unacceptable: payments, user actions, state changes.
- Use delivery mode 1 for high-throughput, low-value messages where a little loss is tolerable (e.g., telemetry that's aggregated elsewhere).
Variations: Lazy queues and publisher confirms
- Lazy queues: RabbitMQ can store queues entirely on disk (lazy queues) to reduce RAM pressure, at the cost of latency. This pairs well with delivery mode 2 for very large queues.
- Publisher confirms: Enable
publisher_confirmsto know when a message is actually persisted. This gives stronger guarantees than delivery mode alone. - Dead-letter exchanges: For messages that can't be processed, combine with a dead-letter exchange — but it doesn't affect persistence.
Troubleshooting & edge cases
Message lost even with delivery mode 2
- Queue not durable: If the queue is declared without
durable=True, the queue itself won't survive a restart, and messages are lost. Ensure both producer and consumer declare the same durable queue. - Producer didn't set delivery_mode: Double-check your code —
delivery_modemust be set to2in theBasicProperties. A typo or missing parameter silently defaults to mode 1. - Broker crashes before flush: Delivery mode 2 gives a strong guarantee, but it's not atomic. In RabbitMQ, a message is written to disk before ack, but there's a tiny window during a crash where the write may not be flushed. Use publisher confirms and a durable queue to minimize this.
Performance degradation
Persistent messages are slower. If throughput drops, consider batching or using a mix of transient and persistent queues, based on message importance.
"PRECONDITION_FAILED" error
If you try to declare a queue with different durability settings (e.g., one as durable, another as non-durable) with the same name, RabbitMQ throws an error. Always use the same settings across producer and consumer.
What you learned & what's next
You've mastered the core concept: persist messages with delivery mode 2 makes a message durable by writing it to disk, but only if the queue is also durable. You've seen the mental model, implemented it in Python with pika, and learned how to verify persistence across broker restarts. You also know when to use persistent delivery versus transient, and how to troubleshoot common pitfalls.
In the next lesson, we'll explore acknowledgment strategies — how to handle messages that fail processing, and why basic_ack vs basic_nack can be a matter of life or death for your queue. Stay tuned, and keep your messages safe!
Practice recap
Try this: send 10 messages with delivery mode 2 to a durable queue, then restart your RabbitMQ broker without running a consumer. Verify the messages are still in the queue. Then, run the same test with delivery mode 1 and see the difference. This hands-on contrast will make the concept stick.
Common mistakes
- Declaring the queue without
durable=Truewhile using delivery_mode=2 — the queue itself won't survive a restart, so messages are lost. - Forgetting to set
delivery_mode=2inBasicProperties— the message stays transient, and you get no error. - Declaring a queue as durable in one part of your app and non-durable in another, causing a PRECONDITION_FAILED error on restart.
Variations
- Use lazy queues to store messages entirely on disk, reducing memory usage for large or slow-consuming queues.
- Enable publisher confirms to get a guarantee that your message was persisted before moving on.
- Combine delivery mode 2 with a dead-letter exchange to handle messages that fail processing without losing them.
Real-world use cases
- E-commerce order placement — a payment message must not be lost, even if the broker restarts mid-transaction.
- Financial transaction logging where every debit/credit event needs to be durable for audit compliance.
- Inventory update notifications that trigger warehouse actions — losing them could mean overselling stock.
Key takeaways
- Delivery mode 2 persists a message to disk, but only works if the queue itself is durable.
- Always set
delivery_mode=2inBasicPropertiesand declare the queue withdurable=Trueon both producer and consumer. - Persistent delivery has a performance cost — use it for critical messages, not for throwaway data.
- Publisher confirms add an extra guarantee that your message was actually written to disk.
- Test persistence by restarting the broker after sending messages, and check that they survive.
- Mismatched durability settings between producer and consumer cause runtime errors — keep them consistent.
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.