Skip to content

Delivery Guarantees

Every messaging system offers some delivery guarantee. There are three you’ll hear about:

GuaranteeMeaningYou risk
At-most-onceEach message delivered zero or one timeLoss — a message can vanish
At-least-onceEach message delivered one or more timesDuplicates — a message can repeat
Exactly-onceEach message delivered precisely once(the dream)

The uncomfortable truth: in a distributed system with networks and crashes, exactly-once delivery is not achievable. It’s a myth. So the real design choice is between at-most-once and at-least-once — you’re picking which failure you can tolerate: losing a message, or seeing it twice.

Picture the moment a consumer finishes work. It must do two things: process the message, and acknowledge it. A crash can happen between them, and there’s no way to make both atomic across the network:

sequenceDiagram
  participant Q as Queue
  participant C as Consumer
  Q->>C: deliver message
  C->>C: process (do the work)
  Note over C: crash HERE, before ack
  Note over Q: no ack arrived → redeliver
  Q->>C: deliver AGAIN (duplicate)
The unavoidable gap where a duplicate is born

If the consumer crashes after doing the work but before the ack lands, RabbitMQ (correctly, safely) assumes the work never happened and redelivers. The work runs twice. No amount of broker cleverness removes this window — the process and the ack are simply not one atomic step.

That’s why at-least-once is the honest, safe default: RabbitMQ would rather deliver twice than risk losing your message. Choose at-most-once (auto-ack, no persistence) only when the occasional lost message genuinely doesn’t matter — metrics, live dashboards, ephemeral events.

Effectively-once: at-least-once + idempotency

Section titled “Effectively-once: at-least-once + idempotency”

Here’s the good news: you don’t actually need exactly-once delivery. You need exactly-once effect. And you get that by making the consumer idempotent — processing the same message twice produces the same result as processing it once.

Practical ways to be idempotent:

  • Idempotency key. Give each message a stable unique id (an message_id, or a business key like order_id). Before acting, record “I’ve handled id X” in a database (a unique constraint, an upsert, a SETNX). A duplicate finds the key already present and does nothing.
  • Naturally idempotent operations. “Set status = shipped” is safe to run twice; “increment balance by 10” is not. Prefer set/upsert over blind increments where you can.
  • Deduplicate on write. Use a database unique constraint on the business key so a duplicate insert simply fails harmlessly.

Combine RabbitMQ’s at-least-once delivery with an idempotent consumer and you get effectively-once processing — the message might arrive twice, but it only ever takes effect once. That is the correct, achievable target.

Which delivery guarantee does RabbitMQ favour by default, and what is its risk?
Why is exactly-once delivery considered a myth?
What is "effectively-once" processing?
Which is the most naturally idempotent operation?