Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Delivery Semantics

Message delivery semantics define the guarantees a system provides about how many times a message will be delivered and processed.


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.

Delivery Semantics SpectrumAt-Most-OnceAt-Least-OnceExactly-OnceMay lose messagesNever duplicatesSimplest to implementAvoids loss when replicationis healthyMay duplicateRequires idempotent handlingAvoids loss withinKafka transactionsAvoids duplicatesMost complexmorereliablemorecomplex
SemanticMessage LossDuplicatesComplexity
At-most-oncePossibleNeverLow
At-least-onceAvoided when replication is healthyPossibleMedium
Exactly-onceAvoided within Kafka transactionsAvoided within Kafka transactionsHigh

The choice of delivery semantics depends on the use case, with tradeoffs between reliability, complexity, and performance.


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.

ProducerKafkaConsumerProducerProducerKafkaKafkaConsumerConsumersend (fire and forget)acks=0delivercommit offsetcommit before processingprocessIf process fails after commit,message is lost (not redelivered)
# At-most-once producer settings
acks=0 # Don't wait for broker acknowledgment
retries=0 # Don't retry failed sends
// At-most-once: commit before processing
while (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)
}
}
Use CaseWhy Acceptable
Metrics/telemetryIndividual data points are expendable; aggregate matters
Log streamingMissing log entries usually acceptable
Real-time gaming updatesStale data worse than missing data
High-frequency sensor dataNext reading arrives shortly
Failure PointOutcome
Producer crash before sendMessage lost
Network failure during sendMessage may be lost
Consumer crash after commit, before processMessage lost

At-Most-Once Details


In at-least-once delivery, messages are never lost but may be delivered multiple times. The system retries until successful acknowledgment.

ProducerKafkaConsumerProducerProducerKafkaKafkaConsumerConsumersend (acks=all)ackRetries on failuredeliverprocesscommit offsetcommit after processingFailure Scenariodeliver (retry after failure)process (may be duplicate)commit offset
# At-least-once producer settings
acks=all # Wait for all replicas
retries=2147483647 # Retry indefinitely
max.in.flight.requests.per.connection=5 # Default, allows reordering
delivery.timeout.ms=120000 # Total time to deliver
// At-least-once: process before commit
while (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)
}

At-least-once delivery can produce duplicates in several scenarios:

Duplicate ScenariosProducer RetryConsumer Redelivery1. Send2. Timeout (no ack)3. Retry4. Both writes succeed1. Process2. Crash before commit3. Restart4. Reprocess same record

Consumers must be designed to handle duplicates safely:

StrategyImplementation
Natural idempotenceOperations that produce same result regardless of repetition (e.g., SET vs INCREMENT)
Deduplication tableTrack processed message IDs, skip if seen
Upsert semanticsUse database upsert; duplicate writes same data
Idempotency keysInclude unique key in message; external system deduplicates
// Idempotent consumer with deduplication
Set<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
}
Use CaseWhy
Financial transactionsCannot lose data; duplicates handled by business logic
Order processingCannot lose orders; idempotency keys prevent double processing
Event sourcingEvents must not be lost; event IDs enable deduplication
Most production workloadsDefault choice when data must not be lost

At-Least-Once Details


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.

Exactly-Once ComponentsIdempotent ProducerTransactionsTransactional ConsumerProducer IDSequence NumbersAtomic writesCross-partitionread_committedSees only committedtracksenablesguaranteesenablescompletes

Idempotent producers ensure that retries do not create duplicates within a single producer session.

ProducerBrokerProducer(PID: 1000)Producer(PID: 1000)BrokerBrokersend(seq=0)acksend(seq=1)Network timeoutretry send(seq=1)seq=1 already seenack (deduplicated)
ComponentPurpose
Producer ID (PID)Unique identifier assigned by broker
Sequence numberPer-partition sequence; broker detects duplicates
EpochFences zombie producers after failures

Configuration:

enable.idempotence=true # Enable idempotent producer (default in Kafka 3.0+)

Transactions enable atomic writes to multiple partitions and coordination between produce and consume operations.

ProducerTransactionBrokerProducerProducerTransactionCoordinatorTransactionCoordinatorBrokerBrokerinitTransactions()PID assignedbeginTransaction()send(partition-0)send(partition-1)sendOffsetsToTransaction()commitTransaction()write COMMIT markerscommit complete

Configuration:

# Producer
transactional.id=my-app-instance-1 # Unique ID for transaction coordination
enable.idempotence=true # Required for transactions
# Consumer
isolation.level=read_committed # Only see committed transactions

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();
}
}
ScopeEOS Guarantee
Kafka to KafkaFull exactly-once within Kafka
External source → KafkaDepends on source idempotence
Kafka → External sinkRequires sink idempotence or 2PC
EOS CoverageKafkaExternalSourceExternalSinkInput TopicProcessingOutput Topic❓ Source must beidempotent✅ Exactly-once✅ Exactly-once❓ Sink must beidempotent
AspectImpact
LatencyTransaction commit adds latency (~10-50ms typical, workload-dependent)
ThroughputLower than at-least-once due to coordination (workload-dependent)
ComplexityMore failure modes to handle
Resource usageTransaction coordinator memory and CPU
Use CaseWhy
Stream processing pipelinesKafka Streams with EOS for stateful processing
Financial calculationsCannot tolerate duplicates in aggregations
Billing/meteringMust count each event exactly once
Event sourcing with projectionsProjections must be consistent with events

Exactly-Once Details


Can tolerate message loss?yesnoMetrics, logs, high-frequency dataAt-Most-OnceCan handle duplicates(naturally idempotent)?yesnoMost workloads with proper designAt-Least-OnceKafka-to-Kafka only?yesnoKafka Streams, internal pipelinesExactly-OnceMost practical for external systemsAt-Least-Once +External Idempotence
RequirementRecommended SemanticNotes
High throughput, loss acceptableAt-most-onceTelemetry, metrics
Data must not be lostAt-least-onceDefault for most workloads
Duplicates unacceptable, Kafka-onlyExactly-onceKafka Streams, internal processing
Duplicates unacceptable, external systemsAt-least-once + idempotent sinkMore practical than distributed 2PC
ComponentDefaultEOS Support
ProducerAt-least-onceIdempotent producer (enable.idempotence=true)
ConsumerAt-least-onceread_committed isolation
Kafka StreamsAt-least-onceprocessing.guarantee=exactly_once_v2
Kafka ConnectAt-least-onceConnector-dependent

Understanding end-to-end delivery requires considering the entire pipeline:

End-to-End PipelineSourceSystemProducerKafkaBrokerConsumerSinkSystemProducer semantics:acks, retries, idempotenceConsumer semantics:commit timing, isolationSink semantics:idempotence, transactions(1) Read(2) Produce(3) Consume(4) Write

End-to-end exactly-once requires:

  1. Source must not produce duplicates (or producer must deduplicate)
  2. Producer must be idempotent (or transactional)
  3. Consumer must use read_committed (or handle uncommitted reads)
  4. Sink must be idempotent (or support transactions)

The weakest link determines the overall guarantee.