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:

  1. 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.

  2. 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.id mechanism.

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