DevNotes
Support
Apache Kafka Masterclass · Chapter 05

Kafka Producers

The producer send pipeline, a Java producer with sync and async sends and callbacks, serializers, partitioning, batching with linger.ms and batch.size, compression, acks, retries, idempotence and ordering, and the key producer settings.

◷ 7 min read§ 15 sections? 6 interview questions{ } 4 code examples

#Introduction

A producer writes records to Kafka. Sending looks like one method call, but behind it is a pipeline of serialization, partitioning, batching, compression, retries and acknowledgements. Understanding that pipeline explains most producer settings and interview questions.

#The Send Pipeline

send(record)
Serialize key and value into bytes
Partitioner chooses the partition
Record added to a batch for that partition (RecordAccumulator)
Sender thread sends full or expired batches to the partition leaders
Broker writes, replicates per acks, and responds
Callback runs — success with metadata, or an exception

send() is asynchronous: it adds the record to an in-memory buffer and returns a Future immediately. A background sender thread ships batches to brokers.

#A Plain Java Producer

XML
<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>4.1.0</version>
</dependency>
Java
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");

try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {

    ProducerRecord<String, String> record =
            new ProducerRecord<>("orders", "ORD-1001", "{\"status\":\"PLACED\"}");

    producer.send(record, (metadata, exception) -> {
        if (exception == null) {
            System.out.printf("Sent to %s-%d at offset %d%n",
                    metadata.topic(), metadata.partition(), metadata.offset());
        } else {
            System.err.println("Send failed: " + exception.getMessage());
        }
    });
}   // close() flushes buffered records before shutting down
Output
Sent to orders-2 at offset 17

Three Ways to Send

StyleCodeTrade-off
Fire and forgetproducer.send(record)Fastest; failures are silently lost unless you check them
Synchronousproducer.send(record).get()Simple error handling; one round trip per record — very slow
Asynchronous with callbackproducer.send(record, callback)Fast and you still see every failure — the usual choice

Headers

Java
ProducerRecord<String, String> record = new ProducerRecord<>("orders", "ORD-1001", json);
record.headers().add("eventType", "OrderPlaced".getBytes(StandardCharsets.UTF_8));
record.headers().add("traceId", traceId.getBytes(StandardCharsets.UTF_8));

#Serializers

Kafka stores bytes; serializers convert your objects:

SerializerUse
StringSerializer, IntegerSerializer, LongSerializer, ByteArraySerializerSimple types
JSON serializer (Spring Kafka's JsonSerializer, or Jackson yourself)Readable, flexible, no schema enforcement
Avro / Protobuf / JSON Schema serializers with Schema RegistryCompact, typed, schema evolution (see Schemas & Serialization)

#Partitioning

RecordPartition
Partition set explicitlyThat one
Has a keymurmur2(key) % partitions — same key, same partition
No keySticky partitioning — fill a batch for one partition, then switch (since Kafka 2.4, improved in 3.3)

You can plug in a custom Partitioner, but a good key is almost always enough.

#Batching: linger.ms and batch.size

Sending records one by one wastes network round trips. The producer groups records per partition into batches.

SettingDefaultMeaning
batch.size16384 (16 KB)Maximum size of a batch per partition
linger.ms5 ms (Kafka 4.0+; 0 in earlier versions)How long to wait for more records before sending a non-full batch
buffer.memory32 MBTotal memory for unsent records
max.block.ms60 sHow long send() blocks when the buffer is full or metadata is unavailable

A batch is sent when it's full or when linger.ms expires, whichever comes first. Raising linger.ms (e.g. 10–50 ms) and batch.size (e.g. 64–256 KB) improves throughput at the cost of a little latency.

#Compression

Java
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");   // none, gzip, snappy, lz4, zstd
CodecCharacteristics
noneDefault; no CPU cost, most bytes
snappy / lz4Fast, good ratio — common choices
zstdBest ratio with good speed
gzipGood ratio, highest CPU

Compression works on whole batches, so it's more effective with batching enabled. Brokers store the compressed batches as-is and consumers decompress them.

#Acknowledgements (acks)

acksSuccess means…Durability
0SentNone — lost on any failure
1Leader wrote itLost if the leader fails before replication
allAll in-sync replicas have itStrongest — combine with min.insync.replicas=2

Default since Kafka 3.0: acks=all.

#Retries and Delivery Timeout

Transient errors (leader changes, network blips, NotEnoughReplicas) are retried automatically:

SettingDefaultMeaning
retriesInteger.MAX_VALUERetry attempts — effectively bounded by the timeout below
retry.backoff.ms100 msWait between retries (with exponential back-off up to retry.backoff.max.ms)
delivery.timeout.ms120000 (2 min)Total time allowed for a send, including retries — then the callback gets an error
request.timeout.ms30000How long to wait for a single broker response

Non-retriable errors (RecordTooLargeException, serialization errors, authorization failures) fail immediately.

#Idempotence and Ordering

Retries create a risk: the broker writes a batch, the acknowledgement is lost, the producer retries, and the batch is written twice. An idempotent producer prevents this:

  • the broker assigns the producer an ID, and the producer numbers each batch per partition;
  • the broker discards batches it has already written (same producer ID + sequence number).
Java
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);   // default since Kafka 3.0

Idempotence also preserves ordering during retries, provided max.in.flight.requests.per.connection ≤ 5 (the default is 5). Without idempotence, a retried batch could land after a later one.

SettingDefault (Kafka 3.0+)
enable.idempotencetrue
acksall
retriesInteger.MAX_VALUE
max.in.flight.requests.per.connection5

#Message Size

The default maximum record batch size on brokers is about 1 MB (message.max.bytes), with matching producer (max.request.size) and topic (max.message.bytes) limits. For large payloads (files, images), store them in object storage (S3, Azure Blob) and send a reference in the event — the claim-check pattern.

#Key Producer Settings

GoalSettings
Durabilityacks=all, enable.idempotence=true, topic min.insync.replicas=2
ThroughputLarger batch.size, linger.ms 10–50, compression.type=lz4 or zstd
Low latencySmall linger.ms, smaller batches
Bounded failure timedelivery.timeout.ms, max.block.ms

#Interview Questions

Q1Is KafkaProducer.send() synchronous?

No. It appends the record to a buffer and returns a Future; a background sender thread transmits batches. Call .get() to block, or pass a callback to handle the result asynchronously.

Q2What do linger.ms and batch.size control?

Batching. A batch is sent when it reaches batch.size or when linger.ms has passed since it was opened, whichever comes first. Larger values improve throughput and compression at the cost of latency.

Q3What is an idempotent producer?

A producer whose batches carry a producer ID and sequence numbers, so the broker can discard duplicates caused by retries. It's enabled by default since Kafka 3.0 and also keeps per-partition ordering during retries.

Q4What are the acks options?

0 (no acknowledgement), 1 (leader only) and all (all in-sync replicas). all with min.insync.replicas=2 is the durable choice.

Q6How do you send large messages?

Avoid it: store the payload externally and send a reference (claim check). If you must, raise max.request.size, broker message.max.bytes and topic max.message.bytes together, and compress.

#Common Mistakes

  • Creating a producer per message or per request.
  • Fire-and-forget without checking failures.
  • Blocking with .get() on every send in a high-volume path.
  • Slow work inside callbacks.
  • Forgetting close() / flush() on shutdown — buffered records are lost.
  • Disabling idempotence while keeping retries and several in-flight requests — duplicates and reordering.

#Key Takeaways

  • send() is asynchronous; records are batched per partition and sent by a background thread.
  • Use one shared, thread-safe producer, and an async send with a callback.
  • The key decides the partition; no key means sticky partitioning.
  • linger.ms, batch.size and compression tune throughput.
  • Defaults since Kafka 3.0: acks=all, idempotence on, effectively unlimited retries bounded by delivery.timeout.ms.
  • Idempotence prevents retry duplicates — not application-level duplicates.