On this page
Message Semantics
Messages may be lost or duplicated—achieving reliable delivery over an unreliable network involves three semantic guarantees, each representing a different engineering trade-off. At-least-once is the easiest to implement but requires idempotent consumers; exactly-once requires idempotent producers, transactions, and consumer coordination, effectively constructing a distributed transaction on top of the messaging system.
Semantic guarantees in messaging systems (Kafka, RabbitMQ, NATS) are a special case of consistency in distributed systems—sharing the same theoretical foundation but exhibiting different behaviors.
Three Semantics
At-Most-Once
Producer: Send message → Do not wait for ACK → Do not retry
Consumer: Receive message → Process → Acknowledge (before or after processing)
Messages may be lost: Producer does not receive ACK but broker has already received it → Producer does not resend
Consumer acknowledges before processing → Crash → Message has already been removed from the queue
Use cases: Scenarios tolerant of data loss (metrics, logs, non-critical notifications).
At-Least-Once
Producer: Send message → Wait for ACK → Timeout? → Retry
Consumer: Process → Acknowledge → If crash before ack → Message will be redelivered (to another consumer)
Messages may be duplicated: Producer retries → Broker receives 2 copies
Consumer finishes processing but ACK is lost → Broker resends → Consumer receives 2 copies
Use cases: Most messaging systems (Kafka, RabbitMQ confirm mode), with the cost that consumers must handle duplicate messages.
Exactly-Once
Theoretically impossible to guarantee in a fully asynchronous distributed system (a consequence of the FLP impossibility result). In practice, it is approached via two mechanisms:
-
Idempotent Producer (Kafka): The producer assigns a unique identifier (producer_id, sequence_number) to each message. The broker checks the sequence_number—duplicates are discarded. Guarantee: Messages from the same producer to the same partition will not be duplicated.
-
Transactional Writes: Multiple messages from a producer are written atomically within a transaction (commit → all messages become visible; abort → none become visible). Kafka implements this via the
transactional.idmechanism.
The key point: While the broker can deduplicate and ensure atomic visibility, exactly-once semantics on the consumer side require the consumer itself to be idempotent—meaning the consumer cannot use non-idempotent updates like ORDER_STATUS = CONFIRMED.
Idempotent Consumers
Non-idempotent: UPDATE inventory SET count = count + 1 WHERE product_id = 42
→ Retry → count += 1 twice → Incorrect
Idempotent: INSERT INTO orders (order_id, user_id, product_id, status) VALUES (123, 456, 42, 'CREATED')
→ Retry (same order_id=123) → UNIQUE constraint → Ignore/keep existing → Correct
Or: UPDATE inventory SET count = ?, version = version + 1 WHERE product_id = 42 AND version = ?
→ Optimistic locking → First retry succeeds, subsequent retries fail due to version mismatch → "Already processed"
Kafka Idempotent Producer Details
Producer registers with broker for the first time: Obtains a producer_id (assigned by the broker, globally incrementing)
Each message: (producer_id, producer_epoch, base_sequence, [messages])
Broker:
Maintains the last successfully processed sequence_number for each producer
On receiving a new message: if sequence > last_seen + 1 → Determine as "hole"
if sequence <= last_seen → Duplicate → Discard (but record as OK)
else → Append to log
References
- Kafka idempotent producer: kafka.apache.org/documentation/#semantics
- Kafka transactions: kafka.apache.org/documentation/#transactions
Keywords: at-most-once, at-least-once, exactly-once, idempotent producer, transactional outbox, sequence number, deduplication