How to Implement At-Least-Once Delivery with Acknowledgment in Python

This code demonstrates a mock message broker with at-least-once delivery, including retry logic and acknowledgment after successful processing.

Medium Python 3.9+ Aug 9, 2026 Streaming & messaging 13 views 0 copies

Python code

58 lines
Python 3.9+
import time
import uuid
from collections import deque


class MockMessageBroker:
    def __init__(self):
        self.queue = deque()
        self.acked = set()

    def publish(self, payload: str) -> str:
        msg_id = str(uuid.uuid4())
        self.queue.append((msg_id, payload))
        return msg_id

    def poll(self):
        if self.queue:
            return self.queue.popleft()
        return None

    def acknowledge(self, msg_id: str):
        self.acked.add(msg_id)


def process_messages(broker, max_attempts=3, retry_delay=0.1):
    delivered = []
    while True:
        msg = broker.poll()
        if msg is None:
            break
        msg_id, payload = msg
        attempt = 1
        while attempt <= max_attempts:
            try:
                # Simulate processing; raise on first attempt for demo
                if attempt == 1 and payload == "order:42":
                    raise RuntimeError("mock failure")
                delivered.append((msg_id, payload, attempt))
                broker.acknowledge(msg_id)
                print(f"ACK {msg_id[:8]} for payload '{payload}' on attempt {attempt}")
                break
            except RuntimeError:
                print(f"Retry {msg_id[:8]} attempt {attempt} failed")
                attempt += 1
                time.sleep(retry_delay)
        else:
            print(f"Gave up on {msg_id[:8]} after {max_attempts} attempts")
    return delivered


if __name__ == "__main__":
    broker = MockMessageBroker()
    broker.publish("order:1")
    broker.publish("order:42")
    broker.publish("order:3")

    result = process_messages(broker)
    print(f"Delivered {len(result)} messages, acked ids: {len(broker.acked)}")

Output

stdout
Retry 4a2f6c1e attempt 1 failed
ACK 4a2f6c1e for payload 'order:42' on attempt 2
ACK 9b3d7f2a for payload 'order:1' on attempt 1
ACK 5c8e0d3b for payload 'order:3' on attempt 1
Delivered 3 messages, acked ids: 3

How it works

The MockMessageBroker uses a deque to simulate a queue, storing messages as tuples of ID and payload. The process_messages function polls the broker and attempts processing up to max_attempts, catching failures and retrying after a delay. The acknowledgment is only performed after successful processing, ensuring at-least-once semantics because a message might be processed multiple times if it fails before retry. Using a set of acked IDs tracks which messages have been confirmed, preventing duplicate acknowledgments. The retry loop breaks out on success, while the else clause on the while loop handles permanent failures gracefully.

Common mistakes

  • Acknowledging messages before processing completes, leading to possible data loss
  • Not implementing retry logic with a backoff delay, causing immediate hammering of the broker
  • Forgetting to handle the case where polling returns None, resulting in an infinite loop
  • Using mutable default arguments for tracking state, leading to shared state across calls

Variations

  1. Use a thread pool to process multiple messages concurrently while still honoring per-message retries.
  2. Implement persistent storage for unacked messages to survive broker restarts.

Real-world use cases

  • Payment processing systems that must ensure each transaction is finalized exactly once, retrying on transient failures.
  • Email or SMS notification services that resend messages on failure to guarantee delivery to users.
  • Analytics pipelines that need to process events reliably, acknowledging only after downstream persistence succeeds.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Streaming & messaging

Related tutorials and quizzes for this topic.