Skip to content

At-Least-Once Delivery — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about At. We cover key concepts, practical examples, and best practices to help you master this topic.

At-least-once delivery ensures every message is delivered at least once, with possible duplicates. The most common guarantee for reliable messaging systems.

What You'll Learn

By the end of this lesson, you will understand how at-least-once delivery works, how to implement it with manual acknowledgments, how to handle duplicates through idempotency, and when to use this guarantee.

Why It Matters

At-least-once is the default delivery guarantee for most message brokers. It prevents message loss, which is critical for payment processing, order fulfillment, and email delivery. Understanding how it works helps you design consumers that handle duplicates correctly.

Real-World Use

A payment gateway sends transaction events via Webhook. If the consumer crashes after processing but before acknowledging, the gateway sends the event again. The payment is processed twice unless the consumer checks for duplicates.

How At-Least-Once Works

sequenceDiagram
    participant P as Producer
    participant B as Broker
    participant C as Consumer

    P->>B: Publish message
    B->>C: Deliver message
    C->>C: Process message
    C-->>B: Acknowledge (ack)
    Note over C: Consumer crashes before ack
    B->>B: Timeout / detect disconnect
    B->>C: Redeliver message
    C->>C: Process again (duplicate!)

The consumer must acknowledge each message after processing. If the connection drops before the ack arrives, the broker assumes the message was not processed and delivers it again.

Manual Acknowledgment

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='orders', durable=True)
channel.basic_qos(prefetch_count=1)

def callback(ch, method, properties, body):
    print(f"Processing: {body.decode()}")

    try:
        result = process_order(body)
        ch.basic_ack(delivery_tag=method.delivery_tag)
        print("Acknowledged")
    except Exception as e:
        print(f"Failed: {e}")
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)

channel.basic_consume(queue='orders', on_message_callback=callback)
channel.start_consuming()

Expected output:

Processing: {"order_id": "ORD-123"}
Acknowledged

With auto_ack=False (the default), the consumer must call basic_ack. If the consumer crashes before acking, the message is redelivered.

Idempotency: Handling Duplicates

Since at-least-once can produce duplicates, consumers must be idempotent — processing the same message twice produces the same result as processing it once.

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

def process_order(order_id, order_data):
    key = f"processed:order:{order_id}"

    if r.setnx(key, '1'):
        r.expire(key, 86400)
        print(f"Processing order {order_id}")
        return True
    else:
        print(f"Duplicate order {order_id}, skipped")
        return False

process_order('ORD-123', {'amount': 99.99})
process_order('ORD-123', {'amount': 99.99})

Expected output:

Processing order ORD-123
Duplicate order ORD-123, skipped

The idempotency key (SETNX) ensures the order is processed only once. Subsequent deliveries are detected and skipped.

At-Least-Once in Practice

Most message brokers use at-least-once by default:

  • RabbitMQ: Manual acks with auto_ack=False
  • Kafka: Auto-commit offset after polling, with at-least-once when enable.auto.commit=false and manual offset commits
  • SQS: Automatic at-least-once with visibility timeout
# RabbitMQ with publisher confirms + consumer acks
import pika

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.confirm_delivery()
ch.queue_declare(queue='critical', durable=True)

# Producer side: wait for broker confirmation
try:
    ch.basic_publish(
        exchange='', routing_key='critical',
        body='critical data',
        properties=pika.BasicProperties(delivery_mode=2)
    )
    print("Message confirmed at-least-once")
except pika.exceptions.NackError:
    print("Message not confirmed, must retry")

Common Mistakes

1. Using Auto-Ack for Critical Messages

Auto-ack removes messages from the queue as soon as they are delivered. If the consumer crashes, the message is lost. Always use manual acks for important data.

2. Not Handling Duplicates

At-least-once guarantees duplicates will happen. Without idempotency, you charge customers twice, send duplicate emails, or create duplicate records.

3. Acknowledging Before Processing

Acking the message before processing completes means a crash after the ack loses the message. Always ack after successful processing.

4. Re-queuing Without Limit

A message that always fails is re-queued and redelivered infinitely. Track retry count and move persistently failing messages to a dead-letter queue.

5. Confusing At-Least-Once with Exactly-Once

At-least-once guarantees no loss but allows duplicates. Exactly-once guarantees no loss and no duplicates. They are different guarantees with different costs.

Practice Questions

1. How does at-least-once delivery work?

The broker delivers the message and waits for an acknowledgment. If the consumer does not acknowledge (crashes, times out), the broker redelivers the message. This ensures delivery but may create duplicates.

2. What is the main drawback of at-least-once?

Duplicates. Since the consumer can crash after processing but before acknowledging, the same message is delivered and processed multiple times.

3. How do you prevent duplicate processing?

Use idempotency keys. Store processed message IDs in a database or cache. Before processing a message, check if its ID was already processed. If yes, skip it.

4. When should you NOT use at-least-once?

When duplicates are unacceptable and you cannot implement idempotency. Financial systems sometimes require exactly-once. Analytics systems may accept at-most-once for performance.

Challenge

Design an idempotent payment processing consumer that handles at-least-once delivery from RabbitMQ. The payment gateway charges credit cards, and charging the same card twice for the same order is unacceptable. Use a database for idempotency.

FAQ

What is the difference between at-least-once and exactly-once?

At-least-once guarantees delivery but may produce duplicates. Exactly-once guarantees delivery without duplicates and without loss. Exactly-once requires distributed transactions or idempotent consumers.

Does RabbitMQ support exactly-once?

Not natively. RabbitMQ provides at-least-once by default. Exactly-once requires idempotent consumers or the experimental 'stream' queue type.

How does Kafka handle at-least-once?

Kafka consumers commit offsets after processing. If the consumer crashes before committing, offsets are not advanced and the same messages are re-read. This gives at-least-once.

Can I downgrade to at-most-once in RabbitMQ?

Yes, set auto_ack=True. Messages are removed from the queue on delivery. If the consumer crashes, the message is lost. Use this only for non-critical data.

Is at-least-once enough for most applications?

Yes. Most applications can implement idempotency, making duplicates harmless. At-least-once is the standard for production messaging systems.

Mini Project: Idempotent Consumer

import pika
import json
import redis
import hashlib

r = redis.Redis(host='localhost', port=6379, db=0)
IDEMPOTENCY_TTL = 86400

def message_id(body):
    return hashlib.sha256(body).hexdigest()[:16]

def process_safely(ch, method, properties, body):
    mid = message_id(body)

    if r.setnx(f"processed:{mid}", '1'):
        r.expire(f"processed:{mid}", IDEMPOTENCY_TTL)
        data = json.loads(body)
        print(f"Processing: {data}")
        ch.basic_ack(delivery_tag=method.delivery_tag)
    else:
        print(f"Duplicate detected: {mid}, skipping")
        ch.basic_ack(delivery_tag=method.delivery_tag)

conn = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
ch = conn.channel()
ch.queue_declare(queue='orders', durable=True)
ch.basic_qos(prefetch_count=1)
ch.basic_consume(queue='orders', on_message_callback=process_safely)
print("Idempotent consumer started")
ch.start_consuming()

Expected output:

Idempotent consumer started
Processing: {"order_id": "ORD-123"}
Duplicate detected: a1b2c3d4, skipping

What's Next

Now that you understand at-least-once, compare it with at-most-once delivery and learn exactly-once delivery for the strongest guarantee, then explore dead letter queues for failed message handling.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro