Skip to content

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

Kafka Producer Batching

Kafka producers batch multiple messages together before sending to brokers, significantly improving throughput and efficiency. Understanding batching is essential for optimizing producer performance and balancing latency versus throughput trade-offs.

ProducerApplication ThreadsRecordAccumulatorTopic: ordersSender Threadsend()send()send()P0 Batch[msg1, msg2]P1 Batch[msg3]P2 Batch[msg4, msg5, msg6]BrokerMessages accumulatedper partition until:- Batch is full- linger.ms expiresdrain batchessend

AspectWithout BatchingWith Batching
Network requests1 per message1 per batch
Header overheadHigh (repeated headers)Amortized
CompressionPer-messagePer-batch (better ratio)
Broker I/OMany small writesFewer large writes
Throughput vs Batch SizeThroughput vs Batch Size100-byte messagesNo batching: ~10K msg/s16KB batch: ~100K msg/s64KB batch: ~500K msg/sLarger batches = higher throughput(up to network/broker limits)

Illustrative throughput

The throughput figures above are illustrative; actual results vary with message size, hardware, compression, and broker configuration.


The RecordAccumulator is the internal component that manages message batching.

RecordAccumulatorBuffer PoolBatches (per TopicPartition)orders-0orders-1orders-2Memory TrackingFree BuffersAllocated: batch.size × NBatch (building)Batch (ready)Batch (in-flight)Used: X bytesAvailable: Y bytesallocateready to sendtrack
EmptyBuildingReadyIn-FlightCompletedFailedallocate bufferfirst record appendedappend recordsbuffer exhausted (append blocks)batch.size reachedlinger.ms expiredsender drains batchack receivedtimeout/errorretry (if retriable)return bufferreturn buffer + callback error

# Maximum batch size in bytes
batch.size=16384 # 16 KB (default)
# Time to wait for batch to fill
linger.ms=0 # No wait (default)
# Total memory for buffering
buffer.memory=33554432 # 32 MB (default)
# Maximum time to block on send() when buffer full
max.block.ms=60000 # 60 seconds (default)
SettingLow ValueHigh Value
batch.sizeLower latency, lower throughputHigher throughput, more memory
linger.msImmediate send, small batchesLarger batches, added latency
buffer.memoryLess memory, more blockingHigher throughput, more memory

Batch StateEmptyBuildingBuildingBuildingReadySentActionFirst recordlinger timer startsMore records(waiting)linger.ms expiredSender drains0123456789101112131415161718192021222324252627282930313233343536
linger.msBehaviorUse Case
0Send immediately (default)Latency-sensitive
5Wait up to 5msLow-latency with some batching
20Wait up to 20msBalanced throughput/latency
100+Wait up to 100ms+Maximum throughput

A batch is sent when ANY condition is met:

  1. Batch full: batch.size reached
  2. Linger expired: linger.ms elapsed since first record
  3. Explicit flush: producer.flush() called
  4. Close: producer.close() called

When the buffer is exhausted, send() blocks until memory is freed; it does not force a batch to send.


buffer.memory (32 MB)Batch Buffer PoolUsed (16 MB)Free (16 MB)Batch 1Batch 2Batch 3...Buffers are recycledto avoid GC pressure
send() calledBuffer available?yesnoAppend to batchReturn FutureBlock threadmax.block.ms exceeded?yesnoThrow TimeoutExceptionWait for buffer
// Get producer metrics
Map<MetricName, ? extends Metric> metrics = producer.metrics();
// Key metrics
// - buffer-total-bytes: Total buffer memory
// - buffer-available-bytes: Available buffer memory
// - bufferpool-wait-time: Time blocked waiting for buffer

Each partition has its own batch queue:

RecordAccumulatorTopic: ordersPartition 0Partition 1Partition 2Batch (75% full)Batch (100% full, ready)Batch (10% full)P1 batch ready(will send immediately)Other partitions continue filling
BehaviorDescription
Independent batchingEach partition fills independently
Parallel sendsReady batches sent to different brokers in parallel
Uneven fillingHot partitions batch faster

Batch (uncompressed)Batch (compressed)Record 1 (100 bytes)Record 2 (100 bytes)Record 3 (100 bytes)...Record 100 (100 bytes)10 KB totalCompressed payload~3 KB (LZ4)Batch compression:- Better ratio than per-record- Single compress operation- Stored compressed on brokercompress()
# Enable compression for better efficiency
compression.type=lz4
# Larger batches compress better
batch.size=65536 # 64 KB
# Allow time for batch accumulation
linger.ms=20

SenderRecordAccumulatorNetworkClientSenderSenderRecordAccumulatorRecordAccumulatorNetworkClientNetworkClientready(now)ReadyCheckResult{readyNodes, nextReadyMs}drain(readyNodes)Map<Node, List<ProducerBatch>>loop[for each node]Group batches by topic-partitionsend(ProduceRequest)
ProduceRequest {
transactional_id: string (nullable)
acks: int16
timeout_ms: int32
topic_data: [{
topic: string
partition_data: [{
partition: int32
records: RecordBatch // Compressed batch
}]
}]
}

# Large batches
batch.size=131072 # 128 KB
# Wait for batch to fill
linger.ms=50
# Plenty of buffer memory
buffer.memory=134217728 # 128 MB
# Compression for network efficiency
compression.type=lz4
# Multiple in-flight for pipelining
max.in.flight.requests.per.connection=5
# Smaller batches
batch.size=16384 # 16 KB
# Minimal wait
linger.ms=0
# Standard buffer
buffer.memory=33554432 # 32 MB
# No compression (fastest)
compression.type=none
# Still use multiple in-flight
max.in.flight.requests.per.connection=5
# Moderate batch size
batch.size=32768 # 32 KB
# Small wait for accumulation
linger.ms=10
# Adequate buffer
buffer.memory=67108864 # 64 MB
# Light compression
compression.type=lz4
max.in.flight.requests.per.connection=5

MetricDescriptionTarget
batch-size-avgAverage batch sizeClose to batch.size
batch-size-maxMaximum batch sizebatch.size
record-queue-time-avgTime in accumulatorClose to linger.ms
records-per-request-avgRecords per requestHigher = better batching
bufferpool-wait-timeTime waiting for bufferShould be 0
SymptomLikely CauseSolution
Small batch-size-avgToo many partitionsConsolidate or increase linger.ms
Small batch-size-avgLow message rateIncrease linger.ms
High bufferpool-wait-timeBuffer exhaustionIncrease buffer.memory
High record-queue-time-avgSlow broker responseCheck broker health

// Non-blocking - returns immediately
Future<RecordMetadata> future = producer.send(record);
// Optional callback
producer.send(record, (metadata, exception) -> {
if (exception != null) {
handleError(exception);
}
});
// Blocking - waits for broker ack
try {
RecordMetadata metadata = producer.send(record).get();
} catch (ExecutionException e) {
handleError(e.getCause());
}

Synchronous Impact

Synchronous sends defeat batching benefits. Each send waits for response before next send can proceed. Use async with callbacks for production.

// Send accumulated batches immediately
producer.flush(); // Blocks until all batches sent
// Flush before close
producer.flush();
producer.close();

FeatureMinimum Version
Record batching0.8.0
Compression per batch0.8.0
Sticky partitioner2.4.0
Idempotent batching0.11.0
Transactional batching0.11.0