Skip to content

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

Kafka Performance Tuning

This guide covers performance optimization for Kafka producers and consumers, including configuration tuning, batching strategies, compression, and monitoring recommendations.


Producer, network, and consumer performance factorsProducer, network, and consumer performance factorsPerformance FactorsProducerNetworkConsumerBatchingCompressionAcksBuffer SizeBandwidthLatencyConnectionsFetch SizePoll IntervalProcessingParallelism
FactorImpactOptimization Goal
BatchingThroughputLarger batches = higher throughput
CompressionNetwork, storageReduce data size
AcknowledgmentsDurability vs latencyBalance requirements
ParallelismThroughputMatch partitions to consumers

PropertyDefaultThroughputLatency
batch.size16384IncreaseIncrease
linger.ms0IncreaseIncrease
buffer.memory33554432Increase-
compression.typenoneImprovesIncreases CPU
acksallDecreaseIncreases
max.in.flight.requests.per.connection5Increase-
# Batching - larger batches
batch.size=131072 # 128KB
linger.ms=20 # Wait up to 20ms for batch
buffer.memory=134217728 # 128MB total buffer
# Compression
compression.type=lz4 # Fast compression
# Parallel requests
max.in.flight.requests.per.connection=5
# Retries
retries=2147483647
delivery.timeout.ms=120000
# If durability allows
acks=1 # Only leader acknowledgment
# Minimal batching
batch.size=16384 # 16KB
linger.ms=0 # Send immediately
# No compression
compression.type=none
# Quick timeout
request.timeout.ms=10000
# Fast acknowledgment
acks=1
batch.sizelinger.msThroughputLatency
16KB0LowLowest
16KB5MediumLow
64KB10HighMedium
128KB20HighestHigher

These values are example starting points; tune based on payload size and latency targets.

// Monitor batch efficiency
producer.metrics().get("batch-size-avg");
producer.metrics().get("records-per-request-avg");
producer.metrics().get("record-queue-time-avg");

CodecCompression RatioCPUSpeed
none1:1NoneFastest
lz4~2:1LowFast
snappy~2:1LowFast
zstd~3:1MediumMedium
gzip~3:1HighSlow
# Recommended for most workloads
compression.type=lz4
# For maximum compression (CPU available)
compression.type=zstd
# Producer-side compression level (zstd)
compression.zstd.level=3
ScenarioRecommended
High throughput, balancedlz4
Network constrainedzstd
CPU constrainednone or snappy
Storage optimizationzstd
Legacy compatibilitygzip

PropertyDefaultImpact
fetch.min.bytes1Wait for data to accumulate
fetch.max.bytes52428800Maximum per fetch
max.partition.fetch.bytes1048576Maximum per partition
fetch.max.wait.ms500Maximum wait for min.bytes
max.poll.records500Records per poll
# Larger fetches
fetch.min.bytes=65536 # 64KB minimum
fetch.max.bytes=104857600 # 100MB maximum
max.partition.fetch.bytes=10485760 # 10MB per partition
fetch.max.wait.ms=1000 # Wait longer for batches
# More records per poll
max.poll.records=1000
# Longer processing time
max.poll.interval.ms=600000 # 10 minutes
# Immediate fetch
fetch.min.bytes=1
fetch.max.wait.ms=100
# Smaller batches
max.poll.records=100
Consumer parallelism using multiple consumers or a worker thread poolConsumer parallelism using multiple consumers or a worker thread poolParallelism OptionsOption 1: Multiple ConsumersOption 2: Consumer + Thread PoolConsumer 1Partitions 0,1Consumer 2Partitions 2,3Consumer 3Partitions 4,5Consumer(single thread)Worker Pool(many threads)dispatch

Multiple Consumers:

// One consumer per thread
int numConsumers = Math.min(partitionCount, availableCores);
ExecutorService executor = Executors.newFixedThreadPool(numConsumers);
for (int i = 0; i < numConsumers; i++) {
executor.submit(new ConsumerRunnable(createConsumer()));
}

Consumer with Worker Pool:

// Single consumer with async processing
ExecutorService workers = Executors.newFixedThreadPool(10);
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
List<CompletableFuture<Void>> futures = new ArrayList<>();
for (ConsumerRecord<String, String> record : records) {
futures.add(CompletableFuture.runAsync(() -> process(record), workers));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
consumer.commitSync();
}

# Producer
socket.send.buffer.bytes=131072 # 128KB
send.buffer.bytes=131072
# Consumer
socket.receive.buffer.bytes=131072 # 128KB
receive.buffer.bytes=131072
# Connection limits
connections.max.idle.ms=540000 # 9 minutes
reconnect.backoff.ms=50
reconnect.backoff.max.ms=1000
# Request timeouts
request.timeout.ms=30000

# Total buffer memory
buffer.memory=134217728 # 128MB
# Block if buffer full
max.block.ms=60000 # Wait up to 60s

Memory consumption per consumer:

  • Fetch buffer: fetch.max.bytes × assigned partitions
  • Record buffer: Records in poll() result
  • Deserialization overhead
// Monitor consumer memory
consumer.metrics().get("fetch-size-avg");
consumer.metrics().get("records-consumed-rate");
# Cache for reducing writes to state stores
cache.max.bytes.buffering=10485760 # 10MB per thread
# RocksDB memory
rocksdb.config.setter=com.example.CustomRocksDBConfig

MetricDescriptionTarget
record-send-rateRecords sent per secondAs high as needed
record-retry-rateRetries per second< 1% of send rate
batch-size-avgAverage batch sizeClose to batch.size
records-per-request-avgRecords per requestHigher = better batching
request-latency-avgRequest latency< 100ms
outgoing-byte-rateNetwork throughputNetwork capacity
buffer-available-bytesAvailable buffer> 0
buffer-exhausted-rateBuffer full events0
MetricDescriptionTarget
records-consumed-rateRecords consumed per secondMatch production rate
records-lagOffset lagAs low as acceptable
records-lag-maxMaximum lagAlert threshold
fetch-latency-avgFetch latency< 500ms
fetch-rateFetches per secondStable
commit-latency-avgCommit latency< 1000ms
// Access metrics programmatically
Map<MetricName, ? extends Metric> metrics = producer.metrics();
for (Map.Entry<MetricName, ? extends Metric> entry : metrics.entrySet()) {
MetricName name = entry.getKey();
Metric metric = entry.getValue();
if (name.name().equals("record-send-rate")) {
log.info("Send rate: {}", metric.metricValue());
}
}

Terminal window
# Kafka performance test tool
kafka-producer-perf-test.sh \
--topic benchmark \
--num-records 1000000 \
--record-size 1024 \
--throughput -1 \
--producer-props \
bootstrap.servers=kafka:9092 \
batch.size=131072 \
linger.ms=20 \
compression.type=lz4
Terminal window
kafka-consumer-perf-test.sh \
--bootstrap-server kafka:9092 \
--topic benchmark \
--messages 1000000 \
--threads 3
public class ProducerBenchmark {
public static void main(String[] args) {
Properties props = loadConfig();
Producer<String, byte[]> producer = new KafkaProducer<>(props);
int numRecords = 1_000_000;
int recordSize = 1024;
byte[] payload = new byte[recordSize];
long start = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(numRecords);
for (int i = 0; i < numRecords; i++) {
producer.send(
new ProducerRecord<>("benchmark", Integer.toString(i), payload),
(metadata, exception) -> latch.countDown()
);
}
latch.await();
long elapsed = System.currentTimeMillis() - start;
System.out.printf("Sent %d records in %d ms%n", numRecords, elapsed);
System.out.printf("Throughput: %.2f records/sec%n", numRecords * 1000.0 / elapsed);
System.out.printf("Throughput: %.2f MB/sec%n",
numRecords * recordSize / 1024.0 / 1024.0 * 1000.0 / elapsed);
producer.close();
}
}

bootstrap.servers=kafka-1:9092,kafka-2:9092,kafka-3:9092
# Large batches
batch.size=131072
linger.ms=20
buffer.memory=134217728
# Compression
compression.type=lz4
# Parallel requests
max.in.flight.requests.per.connection=5
# Retries
retries=2147483647
delivery.timeout.ms=120000
# Acknowledgments (if durability allows)
acks=1
# Network
send.buffer.bytes=131072
bootstrap.servers=kafka-1:9092,kafka-2:9092,kafka-3:9092
group.id=high-throughput-consumers
# Large fetches
fetch.min.bytes=65536
fetch.max.bytes=104857600
max.partition.fetch.bytes=10485760
fetch.max.wait.ms=1000
# More records
max.poll.records=1000
# Processing time
max.poll.interval.ms=600000
session.timeout.ms=60000
heartbeat.interval.ms=15000
# Manual commit
enable.auto.commit=false
# Assignment
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor
# Producer
batch.size=32768
linger.ms=5
compression.type=lz4
acks=all
# Consumer
fetch.min.bytes=1024
fetch.max.wait.ms=500
max.poll.records=500

SymptomCauseSolution
Small batcheslinger.ms=0Increase linger.ms
High latencyacks=allConsider acks=1 if safe
CPU boundCompressionUse faster codec
Network boundNo compressionEnable compression
SymptomCauseSolution
Long queue timeLarge batchesReduce batch.size, linger.ms
Slow acksacks=all, slow replicasCheck replica health
Network delayHigh latency networkIncrease timeouts
SymptomCauseSolution
Growing lagSlow processingIncrease parallelism
Frequent rebalancesLong processingIncrease max.poll.interval.ms
Small fetchesfetch.min.bytesIncrease fetch size

PracticeRecommendation
Measure firstBenchmark before tuning
Change one thingIsolate variable impact
Monitor continuouslyTrack performance metrics
Test in production-likeUse realistic data and load
PracticeRecommendation
Use compressionLZ4 for most workloads
Tune batchingBalance throughput and latency
Handle backpressureMonitor buffer.memory usage
Use callbacksHandle delivery results
PracticeRecommendation
Match partitionsConsumers <= Partitions
Process asyncFor CPU-intensive work
Commit carefullyAfter processing, not before
Monitor lagAlert on threshold breach