Siddhant Deval
Siddhant Deval
backend16 min read

Producer Tuning, Compression, and Capacity Planning

Producer throughput and broker load are shaped by three orthogonal knobs — batching (linger.ms, batch.size), compression codec, and partition count. Getting any one wrong forces a costly live repartition or compression migration under traffic. This article provides the engineering model for each decision, including the partition count sizing formula and the zstd vs snappy vs lz4 trade-off matrix.

Producer Tuning, Compression, and Capacity Planning

The order event pipeline was working fine at 5,000 messages per second. At 50,000 per second, the Kafka brokers are saturated — disk I/O at 95%, network egress pegged. The team adds more brokers and partitions. The saturation persists. The actual problem is that each producer is sending one message per network round trip with no batching (linger.ms=0, the default), generating 50,000 individual write requests per second to the broker. Each request incurs TCP framing, broker log append, ISR synchronisation overhead, and ack serialisation — multiplied by 50,000. With linger.ms=5 and batch.size=65536, the same 50,000 messages are grouped into ~800 batched requests per second. The broker I/O drops 60×.

Producer tuning is not premature optimisation. The three knobs — batching, compression, and partition count — are architectural decisions made at the start of a system's lifecycle. Changing them under live traffic is costly and risky.

Architectural Note

Series positioning: This is Part 11 of Distributed Messaging Systems. It pairs with Part 6 (Consumer Patterns) to complete the full producer-to-consumer pipeline performance picture. The partition count sizing model builds on Scaling: Partitioning, Sharding, and Replication.


1. Batching: linger.ms and batch.size

1.1 How Kafka Batching Works

Kafka's producer accumulates messages into a RecordBatch before sending. Two thresholds control when a batch is flushed:

  • batch.size (default: 16,384 bytes = 16 KB): flush when the batch reaches this size
  • linger.ms (default: 0): flush after this many milliseconds even if batch is not full
  • The batch sends when either threshold is crossed — whichever comes first
TYPESCRIPT
// ❌ Default settings — one network round-trip per message
const producer = kafka.producer({
  // linger.ms = 0 (default): send immediately, never wait for batch to fill
  // batch.size = 16384 (default): 16 KB max batch — rarely reached at linger.ms=0
})
// At 1,000 msg/s: 1,000 broker write requests/s
// At 50,000 msg/s: 50,000 broker write requests/s — network / I/O saturated

// ✅ Tuned for throughput — batch messages over 5ms window
const producer = kafka.producer({
  // kafkajs equivalent: configure via KafkaJS producer options
  maxInFlightRequests: 5,
})

// In kafkajs, batching is controlled at the send() level with batch API:
await producer.sendBatch({
  topicMessages: [{
    topic:    'orders.created',
    messages: events.map(e => ({
      key:   e.orderId,
      value: JSON.stringify(e),
    }))
  }]
  // Kafka client groups all messages into one or more RecordBatches
  // based on partition assignment and batch.size
})

// For Java/confluent clients (where linger.ms is explicit):
// props.setProperty("linger.ms", "5");
// props.setProperty("batch.size", String.valueOf(64 * 1024)); // 64 KB

1.2 The Batching Trade-off

linger.ms batch.size Latency Throughput Best for
0 16 KB Lowest (immediate send) Low Interactive, latency-critical
5 64 KB +5 ms High Balanced — safe default
20 256 KB +20 ms Very high Bulk ingestion, analytics
100 1 MB +100 ms Maximum ETL, batch-only pipelines
Pro Tip & Optimization

linger.ms=5 and batch.size=65536 (64 KB) is the safe starting point for most workloads. Tune upward only after measuring broker write amplification — the ratio of individual produce requests to actual messages. If write amplification is near 1.0, batching is not working. If it is near 0.02 (1 request per 50 messages), batching is effective.

1.3 Measuring Batching Effectiveness

BASH
# JMX metric: records-per-request-avg
# > 10: good batching (10+ messages per network request)
# < 2:  poor batching (near one message per request — check linger.ms)

# kafkajs: measure at application level
let batchCount    = 0
let messageCount  = 0

const originalSendBatch = producer.sendBatch.bind(producer)
producer.sendBatch = async (batch) => {
  const count = batch.topicMessages.reduce((sum, t) => sum + t.messages.length, 0)
  batchCount++
  messageCount += count
  metrics.histogram('producer.batch.size', count)
  return originalSendBatch(batch)
}
// Alert if avg batch size < 5 on high-throughput producers

2. Compression

2.1 Codec Comparison

TYPESCRIPT
// Compression is set at the producer level and applies per RecordBatch
// KafkaJS: zstd requires the @kafkajs/zstd plugin — not bundled in core
// npm install --save @kafkajs/zstd
import { CompressionTypes, KafkaConfig } from 'kafkajs'
import { ZstdCodec }                     from '@kafkajs/zstd'

// Register the codec once at app startup
CompressionCodecs[CompressionTypes.ZSTD] = ZstdCodec

const producer = kafka.producer({
  createPartitioner: Partitioners.LegacyPartitioner,
})

await producer.send({
  topic:       'orders.created',
  compression: CompressionTypes.ZSTD,   // ✅ modern default (after codec registration)
  messages:    messages,
})
Codec Compression ratio CPU cost Decompression speed Recommended
none 1× (no compression) None N/A Only for binary payloads already compressed
gzip Best (5–7×) High Slow ❌ Legacy only — CPU expensive
snappy Moderate (2–4×) Low Fast Low-CPU environments only
lz4 Moderate (3–5×) Very low Fastest Latency-critical, small messages
zstd Best-in-class (5–8×) Moderate Fast ✅ Modern default — all new topics
Crucial Requirement

zstd is the modern default. It achieves the best compression ratio at moderate CPU cost. In KafkaJS, zstd is not bundled — install @kafkajs/zstd and register CompressionCodecs[CompressionTypes.ZSTD] = ZstdCodec at startup. Requires Kafka broker 2.1+ and the matching client codec on every consumer. For Java/Confluent clients, zstd is available natively — no additional package required.

2.2 Where Compression Happens

Pro Tip & Optimization

The Kafka broker stores and forwards the compressed batch without decompressing. CPU for compression is paid once at the producer; CPU for decompression is paid once at the consumer. The broker incurs zero decompression CPU — this is why Kafka recommends enabling compression by default for all topics with JSON or text payloads.

2.3 Compression and Message Size

TYPESCRIPT
// ❌ Individual message compression — wrong level
// Compression works on batches, not individual messages
// A single 100-byte JSON message compresses poorly — compression overhead > savings

// ✅ Ensure batch is large enough for compression to be effective
// Minimum effective batch for compression: ~10 KB uncompressed
// At linger.ms=5, batch.size=64 KB:
//   100-byte messages: ~640 messages per batch → ~4 KB compressed (zstd)
//   1 KB messages: ~64 messages per batch → ~8 KB compressed
// Both achieve significant compression ratio at reasonable batch sizes

3. Partition Count Sizing

3.1 The Sizing Formula

partitions = ceil(target_throughput_MB/s / min(producer_MB/s_per_partition, consumer_MB/s_per_partition))
TYPESCRIPT
// Sizing example:
const targetThroughputMBps    = 100   // 100 MB/s peak ingest
const producerMBpsPerPartition = 10   // measured: broker handles 10 MB/s per partition
const consumerMBpsPerPartition = 50   // consumer can process 50 MB/s per partition

const bottleneck   = Math.min(producerMBpsPerPartition, consumerMBpsPerPartition) // 10
const minPartitions = Math.ceil(targetThroughputMBps / bottleneck)                // 10

// Apply headroom multiplier for future growth
const recommendedPartitions = minPartitions * 2  // 20 — room to double throughput without repartition

3.2 Partition Count Constraints

Constraint Impact Recommendation
Min: consumer parallelism Partitions ≥ max expected consumer count Plan for peak scale-out
Max: broker overhead Each partition = open file descriptor + leader election state < 4,000 partitions per broker
Ordering Ordering guaranteed only within a partition Partition key must capture ordering scope
Immutability Partition count can be increased, never decreased Provision 2–4× headroom upfront

3.3 The Live Repartition Problem

Performance / Safety Warning

Never repartition a live topic without a consumer group migration plan. After increasing partition count, the hash ring changes — the same key maps to a different partition. Existing in-flight messages for a key will be on the old partition; new messages will be on the new partition. Any consumer that joins after repartitioning will see events for the same business entity out of order. The safe procedure always includes draining to lag=0 before the partition count change.


4. acks and Durability

4.1 The Three Durability Levels

TYPESCRIPT
// acks=0: fire and forget — fastest, zero durability guarantee
await producer.send({
  topic:    'metrics.raw',
  acks:     0,   // producer does not wait for broker acknowledgement
  messages: [{ value: JSON.stringify(metric) }]
})
// Throughput: maximum. Data loss: possible on any broker crash.
// Use only for metrics and telemetry where loss is acceptable.

// acks=1: leader ack — fast, durability until leader crashes
await producer.send({
  topic:    'logs.application',
  acks:     1,   // leader writes to its local log and acks — followers not guaranteed
  messages: [{ value: JSON.stringify(logEntry) }]
})
// Throughput: high. Risk: if leader crashes before ISR replication, message is lost.

// acks=-1 (all): ISR quorum — slower, highest durability
await producer.send({
  topic:    'orders.created',
  acks:     -1,  // all ISR replicas must write before ack is sent
  messages: [{ value: JSON.stringify(order) }]
})
// Throughput: moderate. Risk: lost only if all ISR replicas fail simultaneously.
// Required for at-least-once and exactly-once delivery guarantees.
acks Durability Latency added Use case
0 None 0 ms Metrics, sampling, telemetry
1 Leader only ~1 ms Application logs, non-critical events
-1 (all) Full ISR quorum ~5–20 ms Business events, financial data

4.2 Idempotent Producer

TYPESCRIPT
// ✅ Idempotent producer: prevents duplicate messages from producer retries
// Required for exactly-once and strongly recommended for at-least-once
const producer = kafka.producer({
  idempotent:          true,   // enables producer sequence numbers
  maxInFlightRequests: 5,      // idempotent producers can have up to 5 in-flight requests
  // Kafka enforces: if the same sequence is received twice, the second is a no-op
})
// Effect: producer retries on broker ack timeout no longer produce duplicates
// Cost: none — sequence numbers are broker-side bookkeeping, zero application change

Summary

Concept Rule
Batching defaults linger.ms=5 + batch.size=64 KB is a safe starting point for most throughput workloads; tune upward only after measuring broker write amplification.
Compression default zstd is the modern default compression codec: best compression ratio, moderate CPU — prefer it over gzip (slow) or snappy (low ratio).
Partition immutability Never repartition a live topic without a consumer group migration plan; ordering within the same key breaks during the transition window.

What's Next

Part 12: Testing Async Systems — Contract Tests, Embedded Brokers, and Chaos Injection closes Series 1 with the testing problem that every async system eventually faces: how do you write reliable, fast, deterministic tests for a system whose correctness depends on a broker you don't control? Embedded Kafka, mock brokers, Pact contract tests, and targeted chaos injection give you the full testing pyramid for async pipelines.

Research & Synthesis Note

This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.

#Kafka#Producer Tuning#Compression#Capacity Planning#Performance#Backend#Distributed Systems
Siddhant Deval

Written by Siddhant Deval

Senior Full-Stack Engineer building high-scale architectures, browser performance engineering systems, and SaaS platforms.