Delivery Semantics
Message delivery semantics define the guarantees a system provides about how many times a message will be delivered and processed.
Overview
Section titled “Overview”Distributed systems face fundamental challenges in message delivery due to network failures, process crashes, and the impossibility of distinguishing between a slow system and a failed one. Delivery semantics describe the guarantees a system can provide despite these challenges.
| Semantic | Message Loss | Duplicates | Complexity |
|---|---|---|---|
| At-most-once | Possible | Never | Low |
| At-least-once | Avoided when replication is healthy | Possible | Medium |
| Exactly-once | Avoided within Kafka transactions | Avoided within Kafka transactions | High |
The choice of delivery semantics depends on the use case, with tradeoffs between reliability, complexity, and performance.
At-Most-Once
Section titled “At-Most-Once”In at-most-once delivery, messages may be lost but are never duplicated. The producer sends and does not wait for acknowledgment, or acknowledges before processing.
Producer Configuration
Section titled “Producer Configuration”# At-most-once producer settingsacks=0 # Don't wait for broker acknowledgmentretries=0 # Don't retry failed sendsConsumer Pattern
Section titled “Consumer Pattern”// At-most-once: commit before processingwhile (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); consumer.commitSync(); // Commit first for (ConsumerRecord<String, String> record : records) { process(record); // Then process (may fail after commit) }}When to Use At-Most-Once
Section titled “When to Use At-Most-Once”| Use Case | Why Acceptable |
|---|---|
| Metrics/telemetry | Individual data points are expendable; aggregate matters |
| Log streaming | Missing log entries usually acceptable |
| Real-time gaming updates | Stale data worse than missing data |
| High-frequency sensor data | Next reading arrives shortly |
Failure Scenarios
Section titled “Failure Scenarios”| Failure Point | Outcome |
|---|---|
| Producer crash before send | Message lost |
| Network failure during send | Message may be lost |
| Consumer crash after commit, before process | Message lost |
At-Least-Once
Section titled “At-Least-Once”In at-least-once delivery, messages are never lost but may be delivered multiple times. The system retries until successful acknowledgment.
Producer Configuration
Section titled “Producer Configuration”# At-least-once producer settingsacks=all # Wait for all replicasretries=2147483647 # Retry indefinitelymax.in.flight.requests.per.connection=5 # Default, allows reorderingdelivery.timeout.ms=120000 # Total time to deliverConsumer Pattern
Section titled “Consumer Pattern”// At-least-once: process before commitwhile (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100)); for (ConsumerRecord<String, String> record : records) { process(record); // Process first } consumer.commitSync(); // Then commit (if crash before commit, redelivered)}The Duplicate Problem
Section titled “The Duplicate Problem”At-least-once delivery can produce duplicates in several scenarios:
Handling Duplicates: Idempotent Consumers
Section titled “Handling Duplicates: Idempotent Consumers”Consumers must be designed to handle duplicates safely:
| Strategy | Implementation |
|---|---|
| Natural idempotence | Operations that produce same result regardless of repetition (e.g., SET vs INCREMENT) |
| Deduplication table | Track processed message IDs, skip if seen |
| Upsert semantics | Use database upsert; duplicate writes same data |
| Idempotency keys | Include unique key in message; external system deduplicates |
// Idempotent consumer with deduplicationSet<String> processedIds = getProcessedIds(); // Load from persistent store
for (ConsumerRecord<String, String> record : records) { String messageId = record.headers().lastHeader("message-id").value(); if (processedIds.contains(messageId)) { continue; // Skip duplicate } process(record); markProcessed(messageId); // Persist to deduplication store}When to Use At-Least-Once
Section titled “When to Use At-Least-Once”| Use Case | Why |
|---|---|
| Financial transactions | Cannot lose data; duplicates handled by business logic |
| Order processing | Cannot lose orders; idempotency keys prevent double processing |
| Event sourcing | Events must not be lost; event IDs enable deduplication |
| Most production workloads | Default choice when data must not be lost |
Exactly-Once Semantics (EOS)
Section titled “Exactly-Once Semantics (EOS)”Exactly-once semantics ensure Kafka transactional read-process-write pipelines process committed records exactly once. Kafka achieves this through idempotent producers, transactions, and transactional consumers.
How Kafka EOS Works
Section titled “How Kafka EOS Works”1. Idempotent Producers (KIP-98)
Section titled “1. Idempotent Producers (KIP-98)”Idempotent producers ensure that retries do not create duplicates within a single producer session.
| Component | Purpose |
|---|---|
| Producer ID (PID) | Unique identifier assigned by broker |
| Sequence number | Per-partition sequence; broker detects duplicates |
| Epoch | Fences zombie producers after failures |
Configuration:
enable.idempotence=true # Enable idempotent producer (default in Kafka 3.0+)2. Transactions
Section titled “2. Transactions”Transactions enable atomic writes to multiple partitions and coordination between produce and consume operations.
Configuration:
# Producertransactional.id=my-app-instance-1 # Unique ID for transaction coordinationenable.idempotence=true # Required for transactions
# Consumerisolation.level=read_committed # Only see committed transactions3. Read-Process-Write Pattern
Section titled “3. Read-Process-Write Pattern”The canonical exactly-once pattern: consume from input topic, process, produce to output topic, all atomically.
producer.initTransactions();
while (true) { ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
producer.beginTransaction(); try { for (ConsumerRecord<String, String> record : records) { ProducerRecord<String, String> output = process(record); producer.send(output); }
// Commit consumer offsets as part of transaction producer.sendOffsetsToTransaction( getOffsetsToCommit(records), consumer.groupMetadata() );
producer.commitTransaction(); } catch (Exception e) { producer.abortTransaction(); }}EOS Scope and Limitations
Section titled “EOS Scope and Limitations”| Scope | EOS Guarantee |
|---|---|
| Kafka to Kafka | Full exactly-once within Kafka |
| External source → Kafka | Depends on source idempotence |
| Kafka → External sink | Requires sink idempotence or 2PC |
EOS Performance Considerations
Section titled “EOS Performance Considerations”| Aspect | Impact |
|---|---|
| Latency | Transaction commit adds latency (~10-50ms typical, workload-dependent) |
| Throughput | Lower than at-least-once due to coordination (workload-dependent) |
| Complexity | More failure modes to handle |
| Resource usage | Transaction coordinator memory and CPU |
When to Use Exactly-Once
Section titled “When to Use Exactly-Once”| Use Case | Why |
|---|---|
| Stream processing pipelines | Kafka Streams with EOS for stateful processing |
| Financial calculations | Cannot tolerate duplicates in aggregations |
| Billing/metering | Must count each event exactly once |
| Event sourcing with projections | Projections must be consistent with events |
Choosing Delivery Semantics
Section titled “Choosing Delivery Semantics”Decision Matrix
Section titled “Decision Matrix”| Requirement | Recommended Semantic | Notes |
|---|---|---|
| High throughput, loss acceptable | At-most-once | Telemetry, metrics |
| Data must not be lost | At-least-once | Default for most workloads |
| Duplicates unacceptable, Kafka-only | Exactly-once | Kafka Streams, internal processing |
| Duplicates unacceptable, external systems | At-least-once + idempotent sink | More practical than distributed 2PC |
Semantic Guarantees by Component
Section titled “Semantic Guarantees by Component”| Component | Default | EOS Support |
|---|---|---|
| Producer | At-least-once | Idempotent producer (enable.idempotence=true) |
| Consumer | At-least-once | read_committed isolation |
| Kafka Streams | At-least-once | processing.guarantee=exactly_once_v2 |
| Kafka Connect | At-least-once | Connector-dependent |
End-to-End Delivery
Section titled “End-to-End Delivery”Understanding end-to-end delivery requires considering the entire pipeline:
End-to-end exactly-once requires:
- Source must not produce duplicates (or producer must deduplicate)
- Producer must be idempotent (or transactional)
- Consumer must use read_committed (or handle uncommitted reads)
- Sink must be idempotent (or support transactions)
The weakest link determines the overall guarantee.
Related Documentation
Section titled “Related Documentation”- At-Most-Once - Fire and forget patterns
- At-Least-Once - Retry with idempotent consumers
- Exactly-Once - Transactions and EOS deep-dive
- Choosing Semantics - Decision guide
- Producer Guide - Producer configuration
- Consumer Guide - Consumer patterns