8.2 Latency vs Throughput Tuning
Module Overview
Goal: Understand the latency/throughput trade-off and optimize for your use case.
What You'll Learn:
- The fundamental latency/throughput trade-off
- Configurations that impact latency
- Configurations that impact throughput
- Tuning for different scenarios
- Measuring and monitoring performance
- Finding the sweet spot for your workload
Prerequisites:
- Module 7.1 (Producer Internals)
- Module 8.1 (Batching & Buffering)
1. The Fundamental Trade-Off
Latency vs Throughput
Low Latency High Throughput
│ │
│ │
Send immediately Wait and batch
Small batches Large batches
No compression Compress data
acks=1 acks=all
│ │
↓ ↓
Few msgs per request Many msgs per request
More network overhead Less network overhead
Key Insight: You cannot maximize both simultaneously. Every optimization for one impacts the other.
Why the Trade-Off Exists
Latency optimization requires:
- Sending immediately (no batching delay)
- Small batches (less waiting to fill)
- Minimal processing (no compression)
Throughput optimization requires:
- Batching (introduces delay)
- Large batches (more waiting)
- Compression (CPU overhead, but network savings)
2. Latency-Focused Tuning
Goal: Minimize Time from Send to Broker Acknowledgment
Target: < 10ms p99 latency
Configuration for Low Latency
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Latency-optimized settings
props.put("linger.ms", 0); // Send immediately
props.put("batch.size", 8192); // 8 KB (small batches)
props.put("acks", "1"); // Leader ack only
props.put("compression.type", "none"); // No compression overhead
props.put("buffer.memory", 16777216); // 16 MB (sufficient)
props.put("max.in.flight.requests.per.connection", 5); // Allow pipelining
Latency Impact of Each Setting
| Configuration | Value for Low Latency | Impact |
|---|---|---|
linger.ms |
0 | No artificial delay |
batch.size |
8 KB | Small batches fill quickly |
acks |
1 | Faster acknowledgment |
compression.type |
none | No compression overhead |
max.in.flight.requests |
5 | Pipeline requests (lower wait) |
Expected Performance
Configuration: Low latency
Message size: 1 KB
Message rate: 1,000/sec
Results:
Throughput: ~50 MB/sec
Latency p50: 3ms
Latency p99: 8ms
Requests/sec: ~500 (many small requests)
3. Throughput-Focused Tuning
Goal: Maximize Messages per Second
Target: > 100,000 msgs/sec
Configuration for High Throughput
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Throughput-optimized settings
props.put("linger.ms", 20); // Wait to batch more
props.put("batch.size", 65536); // 64 KB (large batches)
props.put("acks", "1"); // Balance (not "all")
props.put("compression.type", "lz4"); // Efficient compression
props.put("buffer.memory", 67108864); // 64 MB (absorb spikes)
props.put("max.in.flight.requests.per.connection", 5);
Throughput Impact of Each Setting
| Configuration | Value for High Throughput | Impact |
|---|---|---|
linger.ms |
10-100ms | More records per batch |
batch.size |
64-128 KB | Fewer, larger requests |
compression.type |
lz4, snappy | Network bandwidth savings |
buffer.memory |
64-128 MB | Handle traffic spikes |
Expected Performance
Configuration: High throughput
Message size: 1 KB
Message rate: 100,000/sec
Results:
Throughput: ~100 MB/sec
Latency p50: 15ms
Latency p99: 35ms
Requests/sec: ~2,000 (large batches)
4. Configuration Impact Analysis
linger.ms Impact
linger.ms |
Latency Impact | Throughput Impact | Use Case |
|---|---|---|---|
| 0ms | +0ms (no delay) | Baseline | Real-time |
| 1ms | +1ms | +10-20% | Low latency |
| 10ms | +10ms | +50-80% | Balanced |
| 50ms | +50ms | +100-150% | Batch processing |
| 100ms | +100ms | +150-200% | ETL, analytics |
batch.size Impact
batch.size |
Avg Batch Size | Msgs/Request | Throughput |
|---|---|---|---|
| 1 KB | ~0.5 KB | 1-2 | Baseline |
| 8 KB | ~6 KB | 6-8 | +50% |
| 16 KB (default) | ~14 KB | 14-16 | +100% |
| 64 KB | ~60 KB | 60-64 | +200% |
| 128 KB | ~120 KB | 120+ | +300% |
Assumes 1 KB average message size
acks Impact
acks |
Latency | Throughput | Durability |
|---|---|---|---|
| 0 | Lowest (~1ms) | Highest | None |
| 1 | Low (~3-5ms) | High | Medium |
| all | Higher (~10-20ms) | Lower | Highest |
compression.type Impact
| Compression | CPU Overhead | Network Savings | Net Throughput | Latency Impact |
|---|---|---|---|---|
| none | None | 0% | Baseline | +0ms |
| lz4 | Low | 30-50% | +20-40% | +1-2ms |
| snappy | Low | 30-40% | +15-30% | +2-3ms |
| gzip | High | 50-70% | +10-20% | +5-10ms |
| zstd | Medium | 50-60% | +25-35% | +3-5ms |
Varies by data type (text compresses better)
5. Balanced Configuration
Goal: Optimize for Both Latency and Throughput
Target: < 20ms p99 latency, > 50,000 msgs/sec
Balanced Configuration
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// Balanced settings
props.put("linger.ms", 10); // Small batching delay
props.put("batch.size", 16384); // 16 KB (default)
props.put("acks", "1"); // Leader ack
props.put("compression.type", "lz4"); // Efficient compression
props.put("buffer.memory", 33554432); // 32 MB (default)
props.put("enable.idempotence", "true"); // No duplicates
props.put("max.in.flight.requests.per.connection", 5);
Rationale:
linger.ms=10: Brief delay for batching without excessive latencybatch.size=16KB: Default works well for most message sizesacks=1: Balance durability and speedcompression.type=lz4: Fast compression, good ratioenable.idempotence=true: Reliability without major overhead
Expected Performance
Configuration: Balanced
Message size: 1 KB
Message rate: 50,000/sec
Results:
Throughput: ~75 MB/sec
Latency p50: 8ms
Latency p99: 18ms
Requests/sec: ~3,000
6. Scenario-Based Tuning
Scenario 1: Real-Time User Events
Requirements:
- Ultra-low latency (< 5ms p99)
- Moderate volume (5,000 msgs/sec)
- User-facing application
Configuration:
props.put("linger.ms", 0);
props.put("batch.size", 8192);
props.put("acks", "1");
props.put("compression.type", "none");
Trade-off: Accept lower throughput for minimal latency.
Scenario 2: IoT Sensor Data
Requirements:
- High volume (100,000 msgs/sec)
- Small messages (100 bytes)
- Latency acceptable (< 50ms)
Configuration:
props.put("linger.ms", 20);
props.put("batch.size", 32768); // 32 KB
props.put("acks", "1");
props.put("compression.type", "lz4");
Trade-off: Brief latency for maximum throughput.
Scenario 3: Financial Transactions
Requirements:
- Maximum durability (no data loss)
- Low volume (1,000 msgs/sec)
- Latency acceptable (< 100ms)
Configuration:
props.put("linger.ms", 0);
props.put("batch.size", 16384);
props.put("acks", "all"); // Max durability
props.put("enable.idempotence", "true");
props.put("compression.type", "lz4");
Trade-off: Higher latency for guaranteed delivery.
Scenario 4: Log Aggregation
Requirements:
- Maximum throughput (500,000 msgs/sec)
- Large messages (1 KB average)
- Latency not critical (< 200ms OK)
Configuration:
props.put("linger.ms", 50);
props.put("batch.size", 131072); // 128 KB
props.put("acks", "1");
props.put("compression.type", "lz4");
props.put("buffer.memory", 134217728); // 128 MB
Trade-off: Higher latency for maximum throughput.
7. Measuring Performance
Key Metrics to Track
1. Latency Metrics
kafka.producer:type=producer-metrics,client-id=<id>,name=request-latency-avg
→ Average request latency
kafka.producer:type=producer-metrics,client-id=<id>,name=request-latency-max
→ Maximum request latency
// Application-side (measure end-to-end)
long start = System.currentTimeMillis();
producer.send(record, (metadata, exception) -> {
long latency = System.currentTimeMillis() - start;
// Track latency histogram
});
2. Throughput Metrics
kafka.producer:type=producer-metrics,client-id=<id>,name=record-send-rate
→ Records per second
kafka.producer:type=producer-metrics,client-id=<id>,name=outgoing-byte-rate
→ Bytes per second
3. Efficiency Metrics
kafka.producer:type=producer-metrics,client-id=<id>,name=records-per-request-avg
→ Batching effectiveness
kafka.producer:type=producer-metrics,client-id=<id>,name=compression-rate-avg
→ Compression effectiveness
Benchmarking Example
public class ProducerBenchmark {
public static void main(String[] args) {
KafkaProducer<String, String> producer = createProducer();
int totalRecords = 100000;
long startTime = System.currentTimeMillis();
List<Long> latencies = new ArrayList<>();
for (int i = 0; i < totalRecords; i++) {
long sendStart = System.currentTimeMillis();
producer.send(new ProducerRecord<>("test-topic", "key-" + i, "value-" + i),
(metadata, exception) -> {
long latency = System.currentTimeMillis() - sendStart;
latencies.add(latency);
});
}
producer.flush();
long endTime = System.currentTimeMillis();
// Calculate metrics
long totalTime = endTime - startTime;
double throughput = (totalRecords * 1000.0) / totalTime;
Collections.sort(latencies);
long p50 = latencies.get((int) (latencies.size() * 0.50));
long p99 = latencies.get((int) (latencies.size() * 0.99));
System.out.println("Throughput: " + throughput + " msgs/sec");
System.out.println("Latency p50: " + p50 + " ms");
System.out.println("Latency p99: " + p99 + " ms");
producer.close();
}
}
8. Advanced Techniques
Technique 1: Adaptive Batching
Adjust linger.ms based on load:
public class AdaptiveProducer {
private KafkaProducer<String, String> producer;
private AtomicInteger pendingRecords = new AtomicInteger(0);
public void send(ProducerRecord<String, String> record) {
pendingRecords.incrementAndGet();
producer.send(record, (metadata, exception) -> {
int pending = pendingRecords.decrementAndGet();
// Adjust batching based on load
if (pending > 1000) {
// High load: Increase batching
adjustLingerMs(20);
} else if (pending < 100) {
// Low load: Reduce latency
adjustLingerMs(0);
}
});
}
private void adjustLingerMs(int value) {
// Note: Can't change dynamically, requires restart
// This is conceptual — use separate producers or queues
}
}
Technique 2: Multiple Producers
Use separate producers for different priority levels:
// Low-latency producer for critical messages
KafkaProducer<String, String> criticalProducer = new KafkaProducer<>(
latencyOptimizedConfig());
// High-throughput producer for bulk messages
KafkaProducer<String, String> bulkProducer = new KafkaProducer<>(
throughputOptimizedConfig());
// Route based on priority
if (message.isCritical()) {
criticalProducer.send(record);
} else {
bulkProducer.send(record);
}
9. Common Tuning Mistakes
❌ Mistake 1: Over-Optimizing for Latency
props.put("linger.ms", 0);
props.put("batch.size", 1024); // 1 KB
props.put("compression.type", "none");
Problem: Throughput suffers dramatically (10x worse).
Fix: Accept 5-10ms latency for 2-3x throughput improvement.
❌ Mistake 2: Over-Optimizing for Throughput
props.put("linger.ms", 500); // 500ms!
props.put("batch.size", 1048576); // 1 MB
Problem: 500ms latency unacceptable for most applications.
Fix: Use 10-50ms linger for good balance.
❌ Mistake 3: Ignoring Workload Characteristics
// Same config for all use cases
props.put("linger.ms", 10);
props.put("batch.size", 16384);
Problem: One size doesn't fit all.
Fix: Tune based on actual requirements (latency SLA, message rate, size).
Summary
Key Takeaways
✅ Latency and throughput are inversely related — optimize for your priority
✅ Low latency: linger.ms=0, small batch.size, no compression
✅ High throughput: linger.ms=10-50ms, large batch.size, compression
✅ Balanced: linger.ms=10, batch.size=16KB, lz4 compression
✅ Measure actual performance — don't rely on assumptions
✅ Tune iteratively — adjust one parameter at a time
✅ Consider workload — message rate, size, latency requirements
Decision Matrix
Priority: Latency → Throughput
│ │
↓ ↓
linger.ms: 0 ───→ 1 ───→ 10 ───→ 50 ───→ 100
batch.size: 8KB ───→ 16KB ───→ 64KB ───→ 128KB
compression: none ───→ lz4 ───→ snappy ───→ zstd
acks: 1 ───→ 1 ───→ 1 ───→ all
Tuning Workflow
- Define requirements (latency SLA, throughput target)
- Start with balanced config
- Measure baseline (latency p50/p99, throughput)
- Identify bottleneck (latency too high? throughput too low?)
- Adjust one parameter
- Measure again
- Repeat until targets met
What's Next
Module 8.3: Compression Strategies
Deep dive into compression types and tuningModule 9.1: Practical Producer Patterns
Apply tuning knowledge to real-world patterns
Previous Module: ← 8.1 - Batching & Buffering
Next Module: 8.3 - Compression Strategies →