Skip to content

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

At-Most-Once Delivery

At-most-once delivery guarantees that messages are delivered zero or one time, never more. Messages may be lost but are never duplicated. This semantic provides the highest performance but lowest reliability.


At-Most-Once GuaranteeMessages Sent: 100Messages Delivered: ≤ 100Duplicates: 0Loss scenarios:- Network failure- Broker crash- Consumer crash after commitsome maybe lostneverduplicated
PropertyGuarantee
Delivery count0 or 1
Message lossPossible
DuplicatesNever
OrderingPreserved within partition

The producer sends messages without waiting for acknowledgment.

ProducerKafka BrokerProducerProducerKafka BrokerKafka Brokersend(record)Does not wait for acksend(record)send(record)No confirmation of deliveryMaximum throughputMessages may be lost
# At-most-once producer configuration
acks=0 # No broker acknowledgment
retries=0 # No retry on failure
buffer.memory=67108864 # 64MB buffer
linger.ms=5 # Batch for 5ms
batch.size=32768 # 32KB batch size
max.in.flight.requests.per.connection=1000 # High parallelism
ConfigurationValueRationale
acks=00Producer does not wait for broker acknowledgment
retries=00No retry; failed sends are lost
linger.ms5Small batch window for throughput
batch.size32KBReasonable batch size
max.in.flight.requests.per.connection1000Maximum parallelism
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
props.put(ProducerConfig.ACKS_CONFIG, "0");
props.put(ProducerConfig.RETRIES_CONFIG, 0);
props.put(ProducerConfig.LINGER_MS_CONFIG, 5);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32768);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
Producer<String, String> producer = new KafkaProducer<>(props);
// Fire and forget - no callback, no Future.get()
for (Metric metric : metrics) {
producer.send(new ProducerRecord<>("metrics", metric.key(), metric.toJson()));
}
// Buffer flush (optional, at shutdown)
producer.flush();
producer.close();

Performance Characteristics (Repository Guidance)

Section titled “Performance Characteristics (Repository Guidance)”
Producer PerformanceAt-Most-Once(acks=0)At-Least-Once(acks=1)Strong(acks=all)Throughput: HighestLatency: Lowest (~1ms)CPU: LowestThroughput: HighLatency: Low (~5ms)CPU: LowThroughput: LowerLatency: Higher (~10-50ms)CPU: Higher
MetricAt-Most-OnceAt-Least-OnceExactly-Once
Latency (p50)~1ms~5ms~20ms
ThroughputHighestHighModerate
CPU overheadLowestLowHigher
Network round trips01+Multiple

At-most-once consumers commit offsets before processing, ensuring no redelivery but risking message loss on failure.

ConsumerKafkaProcessingConsumerConsumerKafkaKafkaProcessingProcessingpoll()records [0-99]commitSync(offset=100)commit OKloop[for each record]process(record)May fail hereIf crash after commit, beforeprocess completes, records0-99 are never redelivered
# At-most-once consumer configuration
enable.auto.commit=false # Manual commit control
auto.offset.reset=latest # Skip old messages on new consumer
max.poll.records=500 # Batch size
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "metrics-processor");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Collections.singletonList("metrics"));
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
if (!records.isEmpty()) {
// Commit BEFORE processing (at-most-once)
consumer.commitSync();
// Process after commit - failure here loses messages
for (ConsumerRecord<String, String> record : records) {
processMetric(record.value());
}
}
}

Using auto-commit for at-most-once semantics (example only; not recommended due to timing complexity):

enable.auto.commit=true
auto.commit.interval.ms=100 # Very frequent commits

Auto-Commit Timing

Auto-commit timing does not guarantee at-most-once semantics. Manual commit-before-process provides explicit control.


Producer Failure ScenariosNetwork FailureBuffer OverflowProducer Crash1. send()2. Network fails3. No retry4. Message lost1. Buffer full2. send() blocks/throws3. Message dropped1. Messages in buffer2. Process crash3. Buffer lost
FailureOutcomeMitigation
Network timeoutMessage lostAccept loss
Broker unavailableMessage lostAccept loss
Serialization errorMessage droppedLog error
Buffer fullBlock or exceptionIncrease buffer
Producer crashBuffer lostAccept loss
Consumer StateRunningRunningRunningCRASHEDRestartedOffsetCommitted: 50Committed: 50Committed: 100Committed: 100Committed: 100ProcessingIdlePoll (50-99)ProcessingPartial (50-75)Resume at 100Records 76-99 were committedbut never processed (LOST)01234
Failure PointRecords AffectedOutcome
After commit, before processAll polled recordsLost
During processingRemaining unprocessedLost
After processingNoneNo impact

IoT Telemetry PipelineDevicesKafkaAnalyticsSensor 1Sensor 2Sensor Ntelemetry topic(high throughput)AggregationDashboards1M+ messages/secIndividual loss acceptableAggregate accuracy maintained

Why at-most-once works:

  • High data volume means individual points are expendable
  • Aggregates (averages, percentiles) remain accurate
  • Next reading arrives within seconds
  • Throughput matters more than individual reliability
Data TypeLoss ImpactSemantic
Player position updatesOld data stale anywayAt-most-once
Kill/death eventsMust be recordedAt-least-once
Chat messagesMust not be lostAt-least-once
HeartbeatsExpendableAt-most-once
# Log forwarder configuration (at-most-once acceptable)
acks=0
retries=0
compression.type=lz4 # High compression
linger.ms=100 # Batch aggressively
batch.size=1048576 # 1MB batches

Rationale:

  • Log volume often exceeds retention capacity
  • Missing a few log lines rarely impacts debugging
  • Throughput and cost are primary concerns
  • Duplicates would complicate log analysis

MetricAlert ConditionAction
record-send-totalDropping significantlyCheck connectivity
record-error-rate> 1%Investigate errors
buffer-available-bytes< 10% of totalIncrease buffer or reduce rate
batch-size-avgVery smallIncrease linger.ms
MetricAlert ConditionAction
records-consumed-totalLower than expectedCheck producer health
commit-latency-avgHighCheck broker health
records-lagGrowingScale consumers

At-most-once systems should monitor approximate loss:

// Producer-side counter
AtomicLong sendAttempts = new AtomicLong(0);
AtomicLong sendErrors = new AtomicLong(0);
producer.send(record, (metadata, exception) -> {
sendAttempts.incrementAndGet();
if (exception != null) {
sendErrors.incrementAndGet();
}
});
// Estimated loss rate
double lossRate = (double) sendErrors.get() / sendAttempts.get();

Loss Estimation

With acks=0, the callback exception only captures local errors. Network and broker-side losses are not reported and must be estimated through consumer-side analysis.


// WRONG: Financial transactions must not be lost
producer.send(new ProducerRecord<>("payments", payment.toJson()));
// No ack, no retry - payment may be lost!

Instead: Use at-least-once with idempotent processing:

props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
Anti-Pattern: Mixed SemanticsProducer A(acks=0)Producer B(acks=all)TopicInconsistent behaviorHard to reason aboutMonitoring complexityat-most-oncestrong durability

Instead: Separate topics by reliability requirement:

metrics-realtime → at-most-once (high volume, low value)
metrics-critical → at-least-once (auditing, billing)

AspectAt-Most-OnceAt-Least-OnceExactly-Once
Message lossPossibleNo loss when durability settings are metNever
DuplicatesNeverPossibleNever
LatencyLowestLowHigher
ThroughputHighestHighModerate
ComplexitySimpleModerateComplex
Use caseTelemetry, logsMost workloadsFinancial, billing

When requirements change, migrating from at-most-once to stronger semantics:

# Change from acks=0 to acks=1 or acks=all
acks=all
retries=2147483647
retry.backoff.ms=100
enable.idempotence=true
// Change from commit-before-process to process-before-commit
for (ConsumerRecord<String, String> record : records) {
process(record); // Process first
}
consumer.commitSync(); // Then commit