Current state: "Under Discussion"
Discussion thread: here
JIRA: here
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
Note: This KIP is a result of working with members of Apache Cassandra community who are working on CEP-44: Kafka integration for Cassandra CDC using Sidecar and one of the limitations they have faced was large message sizes
Kafka has a limit for message size which limits some use cases where they might have messages that are larger than message.max.bytes even after enabling compression on the producer side or after applying serialization formats like Apache Avro or Protocol Buffers to reduce payload size. Increasing message size indefinitely is not a viable solution as it can lead to performance degradation, memory issues, and instability of the broker. And by looking at some of the enterprise/cloud offerings of Kafka you can see that on average they can offer 8MB to 10MB as max message size.
At the moment of writing this KIP, there are two famous patterns to handle without increasing message.max.bytes
Break large messages into smaller chunks, send them sequentially, and reassemble on the consumer side.
message.max.bytes.Store the large payload externally and send only a reference (e.g URI, databases key) in the Kafka message.
s3://bucket/key) via Kafka.| Pattern | Pros | Cons |
|---|---|---|
| Chunking | No external storage required | Complex client logic to split and reassemble messages |
| Reference-Based | Minimizes Kafka load | External system dependency |
Composable serializer where the client can apply a list of serializers before applying a large message serializerNote:
This KIP will benefit CEP-44: Kafka integration for Cassandra CDC using Sidecar
In some use-cases we might need a way to be able to chain of Kafka serializer/deserializer before applying large message serializer for example need to apply schema or special format like avro or protobuf. We could just create a single org.apache.kafka.common.serialization.largemessage.Serializer that implemented the Kafka Serializer interface, but we would need to create the different versions (AvroSerializer, ProtobufSerializer) with their support for the schema.
Instead the KIP proposing a composable serializer that allows concatenating several serializers to perform what we are looking for here.
This will be enabled by new configs in Producer/Consumer side
A configurable Kafka serializer that:
large.message.threshold.bytes`):large-message: true headerlarge.message.threshold.bytes):A configurable Kafka deserializer that performs:
large-message: true headerlarge-message` header: large-message header:| Key | Description | Type | Default | Required |
| value.serializers | List of serializers class that implements the `org.apache.kafka.common.serialization.Serializer` interface in order. The first serializer is `Serializer<T>` while the remaining serializers must be `Serializer<byte[]>`. | List<Serializer> | None | No, If not exist the code will fail back to value.serializer. Can't exist with value.serializer |
| key.serializers | List of serializers class that implements the `org.apache.kafka.common.serialization.Serializer` interface in order. The first serializer is `Serializer<T>` while the remaining serializers must be `Serializer<byte[]>`. | List<Serializer> | None | No, If not exist the code will fail back to key.serializer. Can't exist with key.serializer |
| Key | Description | Type | Default | Required |
| value.deserializers | List of deserializer classes that implements the `org.apache.kafka.common.serialization.Deserializer` interface in order. The last deserializer must be `Deserializer<T>` while the rest must be `Deserializer<byte[]>`. | List<Deserialzer> | None | No, If not exist the code will fail back to value.deserializer. Can't exist with value.deserializer |
| key.deserializers | List of deserializer classes that implements the `org.apache.kafka.common.serialization.Deserializer` interface in order. The last deserializer must be `Deserializer<T>` while the rest must be `Deserializer<byte[]>`. | List<Deserialzer> | None | No, If not exist the code will fail back to key.deserializer. Can't exist with key.deserializer |
| Key | Description | Type | Default | Required |
|---|---|---|---|---|
large.message.payload.store.class | Implementation of org.apache.kafka.common.serialization.largemessage.store.PayloadStore . | Class | None | Yes |
large.message.threshold.bytes | The maximum size of the message is considered large. | Long | 1MB (Default of max.message.bytes) | No |
| Key | Description | Type | Default | Required |
|---|---|---|---|---|
large.message.payload.store.class | Implementation of org.apache.kafka.common.serialization.largemessage.store.PayloadStore . | Class | None | Yes |
/**
* An interface for publishing and downloading serialised data to/from payload store.
* The store config will passed down from the original config of the Kafka producer/consumer client.
*/
public interface PayloadStore implements Configurable, Closeable, Monitorable {
/**
* Publish data into the store.
*
* @param data data that will be published to the store.
* @return full path to object in the store.
* @throw PayloadStoreException in case failed to publish to the store.
*/
String publish(String topic, byte[] data) throw PayloadStoreException;
/**
* Download full data from the store.
*
* @param fullPath of the data's reference in the store for example `remote_store/topic_name/<record_random_uuid>`
* @return content of the object as bytes.
* @throw PayloadStoreException
*/
byte[] download(String fullPath) throw PayloadStoreException;
/**
* Generate an id for the data's reference in the store (Not the full path in the store).
* By default the id is a random UUID however some stores might need more smarter way to calculate
* its reference id based on the data itself. In such a case please override this method.
* @param data data that will be published to the store.
* @return object id for example `record_random_uuid`
*/
default String id(byte[] data) {
return UUID.randomUUID().toString();
}
} |
/**
* Exception class that represent exceptions during interaction with the store.
* this helps the Payload store to decided either to retry or to crash.
* The final Serializer and Deserializer will propagate this as SerializationException to client.
**/
public class PayloadStoreException extends RuntimeException {
/**
* Constructor PayloadStoreException with message and throwable.
*/
public PayloadStoreException(String message, Throwable t) {
super(message, t);
}
/**
* Constructor PayloadStoreException with message.
*/
public PayloadStoreException(String message) {
super(message);
}
/**
* Constructor PayloadStoreException with throwable.
*/
public PayloadStoreException(Throwable t) {
super(t);
}
}
|
Map<String, Object> producerConfig = new HashMap<>();
producerConfig.put("value.serializers", "kafka.serializers.KafkaAvroDeserializer, org.apache.kafka.common.serialization.Serializer");
producerConfig.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); producerConfig.put("large.message.payload.store.class", "myclient.serializers.payload.store.CustomS3Store")
producerConfig.put("large.message.threshold.bytes", 1048576);
producerConfig.put("s3.bucket", "my-bucket")
producerConfig.put("s3.retry.attempts", "3");
producerConfig.put("s3.connection.timeout.ms", "5000");
producerConfig.put("bootstrap.servers", "localhost:9092");
KafkaProducer<String, Double> producer = new KafkaProducer<>(producerConfig); |
TTL Configuration Risk: If the payload store owner doesn't configure an appropriate TTL that aligns with Kafka topic retention, the payload store may grow indefinitely. This occurs because objects remain in storage even after Kafka no longer references them, leading to unnecessary storage costs.
TTL Too Short Risk: If the TTL is set too aggressively (shorter than needed), Kafka references may point to objects that no longer exist in the payload store. When this happens:
Consumers will encounter NOT_FOUND errors
To prevent blocking behavior, consumers should capture SerializationException::getCause and decide what to do if the exception is PayloadException/PayloadNotFoundException.
This allows graceful handling of missing payload references
Recommendation: Set TTL duration to exceed your Kafka topic retention period by a reasonable buffer (e.g., 10-20%) to ensure payload availability throughout the message lifecycle while preventing indefinite storage growth.
Critical Timing Constraints: The total timeout for payload store operations (including all retries) cannot exceed `max.block.ms` for Kafka producers or `max.poll.interval.ms` for Kafka consumers. This is a fundamental requirement for any implementation of org.apache.kafka.common.serialization.largemessage.store.PayloadStore.
Exceeding these limits will cause producer blocking or consumer rebalancing issues.
Memory Constrains: Large messages will consume memory during serialization so ensure heap size can accommodate your largest expected message. Consider PayloadStore implementations that support compression to reduce storage footprint.
Are dealing with bulk data (collections, arrays)
Can afford the memory cost during brief serialization
Run on appropriately sized JVMs
Usecases with truly memory-constrained environments probably shouldn't be sending GB-sized messages through Kafka anyway.