At Least Once with Idempotent Consumer in Python
Implements a thread-safe idempotent consumer that processes each unique message exactly once, even when a producer sends duplicates under an at-least-once delivery model.
Python code
45 linesimport threading
import time
import uuid
from collections import Counter
class IdempotentConsumer:
def __init__(self):
self.processed = set()
self._lock = threading.Lock()
def consume(self, message_id, payload):
with self._lock:
if message_id in self.processed:
return "duplicate"
self.processed.add(message_id)
# Simulate work
time.sleep(0.01)
return f"processed: {payload}"
class MockProducer:
def __init__(self, consumer):
self.consumer = consumer
def send_at_least_once(self, payload, times=3):
results = Counter()
for _ in range(times):
msg_id = str(uuid.uuid4())
if self.consumer.consume(msg_id, payload) == "duplicate":
results["duplicate"] += 1
else:
results["processed"] += 1
return dict(results)
if __name__ == "__main__":
consumer = IdempotentConsumer()
producer = MockProducer(consumer)
# Each unique message is processed exactly once, retries are duplicates
first = producer.send_at_least_once("hello")
second = producer.send_at_least_once("hello")
print("First batch:", first)
print("Second batch:", second)
print("Total unique processed:", len(consumer.processed))
Output
First batch: {'processed': 3, 'duplicate': 0}
Second batch: {'processed': 0, 'duplicate': 3}
Total unique processed: 3
How it works
The IdempotentConsumer stores every successfully processed message ID in a set guarded by a threading.Lock. The lock ensures atomic check-and-add, preventing two threads from both processing the same ID. The consume method returns "duplicate" for retries, letting the producer count duplicates. The mock producer mimics at-least-once semantics by generating a fresh UUID for each attempt; with the same message content, the consumer rejects repeat IDs. This pattern guarantees that even if the network or broker retries a send, the work is done once per unique message.
Common mistakes
- Forgetting to check-and-add atomically under the same lock, allowing race conditions
- Using the message payload as the ID instead of a unique message ID
- Not clearing processed IDs for truly ephemeral messages, causing unbounded memory growth
Variations
- Use a Redis SET with expiry (e.g., SETNX) for distributed idempotency across services
- Persist processed IDs in a database table with a unique constraint for durable deduplication
Real-world use cases
- Consuming events from a Kafka-style queue with exactly-once processing semantics.
- Handling webhook deliveries where the sender retries on failure and you must avoid duplicate side effects.
- Processing payment notifications or order updates where reprocessing could charge a customer twice.
Sponsored
More from Reliability & rate limiting
- Build a Rate Limiter Decorator in Python easy
- Build a queue-based admission control system in Python easy
- Chaos Inject Random Failures in Python easy
- Circuit breaker failure threshold count in Python medium
- Exactly Once Processing Dedupe Mock in Python easy
- Fixed Window Counter Rate Limiting in Python easy
Keep learning
Related tutorials and quizzes for this topic.