Status

Current stateDraft

Discussion thread

JIRA:

Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

Motivation

Currently, when a Kafka producer is configured for idempotence (enable.idempotence=true), the configuration max.in.flight.requests.per.connection should be less than or equal to 5. This limitation exists because the Kafka broker hardcodes the number of distinct batch sequence numbers it retains in memory for deduplication (ProducerStateEntry#NUM_BATCHES_TO_RETAIN).

To guarantee idempotent behavior and message ordering, the number of in-flight requests from the producer must not exceed the broker’s ability to track the history of recent sequence numbers. If a producer attempts to send more concurrent requests than the broker tracks, the broker cannot effectively validate sequence gaps or duplicates in the event of out-of-order delivery, leading to OutOfOrderSequenceException or potential ordering violations.

To fully utilize the available bandwidth, the TCP window must be filled. However, the application-level limitation of 5 in-flight requests prevents the producer from filling the pipeline.

$$\text{Throughput} \approx \frac{\text{Request Size} \times \text{In Flight Requests}}{\text{Network RTT}}$$

In scenarios like cross-region requests, hybrid cloud architectures, or multi-availability zone with higher internal latency, the network's Bandwidth-Delay Product (BDP) is high. As Round-Trip Time (RTT) increases and throughput drops linearly because In Flight Requests is artificially capped at 5.

The original limit of 5 was chosen to conserve broker heap memory. However, The memory overhead for tracking sequence numbers is minimal. A BatchMetadata object (tracking sequence/offset) is around 36 bytes.

Even with 10,000 active producers on a single broker, the overhead increases from ~1.8 MB to ~7.2 MB. The modern Kafka deployments often run on instances with significant memory resources. The memory overhead is negligible for many operators compared to the performance penalty incurred by the current throughput cap.

We propose making the broker-side deduplication buffer size configurable (or dynamic). This will allow operators to tune their clusters for high-latency environments by increasing the allowed max.in.flight.requests.per.connection for idempotent producers. This change will enable users to maintain exactly-once semantics and strict ordering without accepting a massive degradation in throughput over long-distance links.

Following is throughput difference among 1, 5, 10 max.in.flight.requests.per.connection. The server is in AWS us-east-1 and the producer is in AWS ap-northeast-1.

max.in.flight.requests.per.connectionthroughputavg latencymax latency50th latency95th latency99th latency99.9th latency
119.623234 records/sec25093.64 ms49788.00 ms25193 ms47286 ms49341 ms49788 ms
595.129376 records/sec5111.89 ms10096.00 ms5135 ms9644 ms10095 ms10096 ms
10 (temporary change hard code value in producer and broker)183.083120 records/sec2558.52 ms5036.00 ms2528 ms4749 ms5034 ms5036 ms

Public Interfaces

Broker Configuration

Introduce a new config on the broker, as the broker must know how much memory to allocate. Operators can set a limitation on the broker side to prevent malicious producers. This configuration only takes effect for idempotent producers.

ProduceRequest / ProduceResopnse

Add a new field MaxIdempotenceBatchesToRetain to reflect broker configuration max.idempotence.batches.to.retain in ProduceResponse.

 {
   "apiKey": 0,
   "type": "request",
   "listeners": ["broker"],
   "name": "ProduceRequest",
   // ...
-  "validVersions": "3-13",
+  // Version 14 is the same as version 13 (KIP-1269).
+  "validVersions": "3-14",
   // ...
 }


 {
   "apiKey": 0,
   "type": "response",
   "name": "ProduceResponse",
   // ...
-  "validVersions": "3-13",
+  // Versions 14 adds MaxIdempotenceBatchesToRetain as a tagged field (KIP-1269).
+  "validVersions": "3-14",
   "flexibleVersions": "9+",
   "fields": [
     // ...
     { "name": "NodeEndpoints", "type": "[]NodeEndpoint", "versions": "10+", "taggedVersions": "10+", "tag": 0,
       "about": "Endpoints for all current-leaders enumerated in PartitionProduceResponses, with errors NOT_LEADER_OR_FOLLOWER.", "fields": [
       { "name": "NodeId", "type": "int32", "versions": "10+",
         "mapKey": true, "entityType": "brokerId", "about": "The ID of the associated node."},
       { "name": "Host", "type": "string", "versions": "10+",
         "about": "The node's hostname." },
       { "name": "Port", "type": "int32", "versions": "10+",
         "about": "The node's port." },
       { "name": "Rack", "type": "string", "versions": "10+", "nullableVersions": "10+", "default": "null",
         "about": "The rack of the node, or null if it has not been assigned to a rack." }
-    ]}
+    ]},
+    { "name": "MaxIdempotenceBatchesToRetain", "type": "int32", "versions": "14+", "taggedVersions": "14+", "tag": 1, "default"
: "5",
+      "about": "The maximum number of idempotence batches the broker retains in memory for a producer to a topic partition."
+    }
   ]
 }


Proposed Changes

Dynamic Capacity Discovery

Initial State (Safety First): When a connection is first established, the produce sends at most max(5, max.in.flight.requests.per.connection) for each node. This ensures safety with any broker version.

Discovery: Upon receiving the first ProduceResponse from a partition leader, the producer checks for the presence of the MaxIdempotenceBatchesToRetain tagged field.

Adaption:

Compatibility, Deprecation, and Migration Plan

New broker with old producer

The minimal value of max.idempotence.batches.to.retain is 5. Even if the old producer sets max.in.flight.requests.per.connection up to 5, it doesn’t break the broker configuration.

Old broker with new producer

The default value of MaxIdempotenceBatchesToRetain is 5, so it works as same constraint in old brokers.


Test Plan

Rejected Alternatives

Hardcoded Increase (e.g., to 20)

Return MaxIdempotenceBatchesToRetain via InitProducerIdResponse