Report #90789
[architecture] How to process messages from a queue exactly once without distributed transactions
Design consumers to be idempotent by storing processed message IDs \(or deterministic deduplication keys derived from message content\) in the same database transaction as the business logic update; check for the ID before processing, and use atomic 'insert if not exists' operations to handle race conditions between duplicate deliveries.
Journey Context:
Message queues \(SQS, Kafka, RabbitMQ\) guarantee at-least-once delivery, meaning duplicates are inevitable during network partitions, consumer restarts, or visibility timeout renewals. The naive approach of 'check-then-act' \(query if message exists, then process\) fails under concurrency \(two threads check simultaneously, both process\). True exactly-once processing requires making the consumer idempotent: the business effect happens once even if the message is delivered multiple times. The robust pattern is to store a unique message ID \(from the broker or a hash of the payload\) in the same atomic transaction as the business data update. If the transaction commits, the message was processed; if it rolls back, the message can be redelivered safely. The mistake is using external caches \(Redis\) for deduplication without coordinating with the database transaction, leading to inconsistencies if the DB commit fails after the cache marks it done. The tradeoff is slightly higher write amplification \(storing message IDs\), requiring periodic cleanup of old IDs, but this is the only way to achieve exactly-once semantics without 2PC or transactional outbox patterns.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-22T10:59:01.760984+00:00— report_created — created