Contents

Backend Development › Queues & Async Processing

At-Least-Once Delivery

Messages may arrive more than once, so consumers must be idempotent.

Also known as: at least once delivery, at-least-once semantics, duplicate delivery, delivery guarantee

At-least-once delivery means the messaging system guarantees every message is delivered one or more times. Nothing is lost, but duplicates can happen. It’s the default for most queues and brokers, and a fact of life in distributed systems.

Why duplicates happen

The broker can’t know whether a consumer finished processing, only whether it acknowledged. If something goes wrong between “did the work” and “acknowledged”, the broker sends the message again (acknowledgement):

consumer receives message ─► processes it (charges the card) ─► crashes before sending ack
broker: no ack → redelivers ─► the card is charged again

Other causes: a processing time longer than the visibility timeout, network failures, rebalancing in a consumer group, a producer retrying a send.

The three guarantees

GuaranteeMeaningThe catch
At-most-onceDelivered 0 or 1 times: no retriesMessages can be lost
At-least-onceDelivered 1 or more timesMessages can be duplicated
Exactly-onceProcessed onceHard and conditional: usually “at-least-once delivery plus idempotent processing” (exactly-once)

For most business systems, losing messages is unacceptable, so at-least-once is chosen, and duplicates are handled by the consumer.

The consequence: make consumers idempotent

If handling the same message twice has the same effect as once, duplicates are harmless (idempotent consumers):

def handle_order_paid(msg):
    if db.already_processed(msg.id):             # remember processed message IDs
        return
    with db.transaction():
        mark_order_paid(msg.order_id)
        db.record_processed(msg.id)              # in the same transaction as the work

Techniques:

  • A unique message ID and a table of processed IDs.
  • Natural idempotence: “set status to paid” can be repeated, unlike “add 100 to the balance”.
  • Unique constraints that reject the duplicate effect.
  • Idempotency keys for calls to external APIs (idempotency key).

Also design for

  • Poison messages that fail every time (poison messages, dead letter queue).
  • Ordering: redelivery can reorder messages.
  • Choosing when to acknowledge: after the work is done, not before, or a crash loses the message.

See delivery guarantees for the whole picture.