Producing
Produce to KubeMQ over the Kafka protocol — message keys and headers, the acks durability setting, batching, and the idempotent producer.
Producing to KubeMQ over the Kafka connector is a straight repoint: every pinned client below
writes to the orders topic on bootstrap.servers=localhost:9092 with no client-library swap
and no code change. Every produced record is written once to the topic's Events Store log, so
whatever a client would do against a real Kafka broker — key-based partitioning, batching,
idempotent retries — carries over unchanged. This guide covers the parts of the produce path
worth understanding before you ship — keys and headers, the acks durability contract,
client-side batching, and the idempotent producer — then walks a full keyed, headered, idempotent
produce in kcat and seven client libraries.
Keys, headers, and partitioning
A record's key decides which partition it lands on; its headers are opaque key/value
pairs that ride alongside the value untouched — a natural home for tracing ids, content types, or
routing metadata your consumers read without touching the payload. Partition assignment for a
keyed record is always decided client-side — KubeMQ never hashes a key itself, it just stores
whatever partition the client chose — and different client libraries ship different default hash
functions (the JVM and franz-go default to murmur2; librdkafka-based clients like kcat default
to CRC32). Mixing producer libraries against the same keyed topic can therefore land the same key
on different partitions, even though every client is behaving correctly by its own rules. An
unkeyed record (key = nil/null) is spread round-robin (or sticky-batched, depending on the
client's partitioner) across the topic's partitions instead. See
Partitions & Ordering for the full
hashing and per-key ordering guarantee, including what happens to that guarantee when a topic's
partition count grows.
Durability: the acks setting
acks controls how many replicas must confirm a write before the producer considers it
acknowledged: 0 (fire-and-forget, no wait), 1 (the leader has written it, but replicas may not
have caught up yet), or all (every in-sync replica has it). This is the single setting that
trades latency for durability, and it's worth setting deliberately rather than leaving at a
client's default. On the next storage engine an acks=all write is fsynced to a quorum of nodes
before it's acknowledged — see
Durability & Retention for the full
contract and how it compares to Apache Kafka's own default posture.
acks=0 is unsafe on a multi-node cluster. A fronting load balancer can land a produce on any
pod. With acks>=1, a follower transparently forwards the write to the leader. With acks=0, a
follower silently drops the record instead of forwarding it — there's no response channel to
signal a redirect. Always use acks>=1 on a multi-node deployment; single-node/standalone setups
are unaffected.
Batching and linger
Client-side batching groups multiple records into one request instead of sending each
individually — batch.size caps the bytes per batch and linger.ms caps how long the client
waits for a batch to fill before sending anyway. A larger linger.ms trades a little added
latency per record for materially higher throughput once you have more than a handful of
producers in flight, since the connector processes one batch instead of many small requests.
kcat, confluent-kafka, kafka-clients, Confluent.Kafka, and both rdkafka bindings all
expose these two settings by name. kafkajs has no linger.ms-equivalent micro-batching —
group multiple records into one send() call to get the same effect. None of this is
KubeMQ-specific configuration; it's ordinary Kafka producer tuning that works unchanged against
the connector.
The idempotent producer
Without idempotence, a producer that times out waiting for an ack and retries can duplicate a
record the broker actually received — the client has no way to tell "lost in transit" from "acked
but the ack was lost." Enabling the idempotent producer (enable.idempotence / idempotent,
depending on the client) closes that gap: each producer session gets a broker-assigned producer
id via InitProducerId, every record it sends carries a monotonically increasing per-partition
sequence number, and the connector deduplicates retries by (producer id, partition, sequence) —
proven to survive a real 3-node leader failover, so a retry after a mid-write leader change still
lands exactly once. Idempotence requires acks=all and a bounded number of in-flight requests
(so retries can't reorder past an unacked send), which every pinned client either defaults or
enforces automatically once idempotence is turned on. franz-go enables idempotent writes by
default — no flag needed, kgo.DisableIdempotentWrite() is the opt-out. See
Limits & rules for the message-size ceiling
idempotent retries still have to respect.
Produce a message
Each example connects with acks=all, batching (batch.size/linger.ms) tuned, and the
idempotent producer enabled, then produces one keyed, headered record to orders.
# order-42:hello kafka (key:value, split by -K:)
echo "order-42:hello kafka" | kcat -b localhost:9092 -t orders -P -K: \
-H "source=demo" \
-X acks=all -X enable.idempotence=true -X linger.ms=50 -X batch.size=65536package main
import (
"context"
"fmt"
"time"
"github.com/twmb/franz-go/pkg/kgo"
)
func main() {
cl, err := kgo.NewClient(
kgo.SeedBrokers("localhost:9092"),
kgo.RequiredAcks(kgo.AllISRAcks()), // acks=all
kgo.ProducerLinger(50*time.Millisecond),
kgo.ProducerBatchMaxBytes(64<<10),
// Idempotent writes are ON by default; kgo.DisableIdempotentWrite() would turn them off.
)
if err != nil {
panic(err)
}
defer cl.Close()
record := &kgo.Record{
Topic: "orders",
Key: []byte("order-42"),
Value: []byte("hello kafka"),
Headers: []kgo.RecordHeader{
{Key: "source", Value: []byte("demo")},
},
}
res := cl.ProduceSync(context.Background(), record)
if err := res.FirstErr(); err != nil {
panic(err)
}
r, _ := res.First()
fmt.Printf("produced to partition=%d offset=%d\n", r.Partition, r.Offset)
}from confluent_kafka import Producer
conf = {
"bootstrap.servers": "localhost:9092",
"acks": "all",
"enable.idempotence": True, # requires acks=all; the client enforces this
"linger.ms": 50,
"batch.size": 65536,
}
producer = Producer(conf)
def on_delivery(err, msg):
if err is not None:
raise RuntimeError(f"delivery failed: {err}")
print(f"produced to partition={msg.partition()} offset={msg.offset()}")
producer.produce(
topic="orders",
key="order-42",
value="hello kafka",
headers=[("source", b"demo")],
callback=on_delivery,
)
producer.flush(10)import java.nio.charset.StandardCharsets;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
public final class Main {
public static void main(String[] args) throws Exception {
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");
props.put("acks", "all");
props.put("enable.idempotence", true); // requires acks=all; the client enforces this
props.put("linger.ms", 50);
props.put("batch.size", 65536);
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
ProducerRecord<String, String> record = new ProducerRecord<>("orders", "order-42", "hello kafka");
record.headers().add("source", "demo".getBytes(StandardCharsets.UTF_8));
RecordMetadata meta = producer.send(record).get();
System.out.printf("produced to partition=%d offset=%d%n", meta.partition(), meta.offset());
}
}
}import { Kafka } from "kafkajs";
async function main(): Promise<void> {
const kafka = new Kafka({ brokers: ["localhost:9092"] });
// kafkajs has no linger.ms; sendBatch (or one messages[] array) is the batching unit.
// idempotent:true requires maxInFlightRequests <= 5 and acks=-1 (all).
const producer = kafka.producer({ idempotent: true, maxInFlightRequests: 5 });
await producer.connect();
const [meta] = await producer.send({
topic: "orders",
acks: -1, // all
messages: [
{
key: "order-42",
value: "hello kafka",
headers: { source: "demo" },
},
],
});
console.log(`produced to partition=${meta.partition} offset=${meta.baseOffset}`);
await producer.disconnect();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});using System.Text;
using Confluent.Kafka;
var config = new ProducerConfig
{
BootstrapServers = "localhost:9092",
Acks = Acks.All,
EnableIdempotence = true, // requires Acks.All; the client enforces this
LingerMs = 50,
BatchSize = 65536,
};
using var producer = new ProducerBuilder<string, string>(config).Build();
var headers = new Headers { { "source", Encoding.UTF8.GetBytes("demo") } };
var result = await producer.ProduceAsync("orders", new Message<string, string>
{
Key = "order-42",
Value = "hello kafka",
Headers = headers,
});
Console.WriteLine($"produced to partition={result.Partition.Value} offset={result.Offset.Value}");require "rdkafka"
config = Rdkafka::Config.new(
:"bootstrap.servers" => "localhost:9092",
:"acks" => "all",
:"enable.idempotence" => true, # requires acks=all; the client enforces this
:"linger.ms" => 50,
:"batch.size" => 65536,
)
producer = config.producer
handle = producer.produce(
topic: "orders",
payload: "hello kafka",
key: "order-42",
headers: { "source" => "demo" },
)
report = handle.wait # blocks until the broker acks
puts "produced to partition=#{report.partition} offset=#{report.offset}"use std::time::Duration;
use rdkafka::config::ClientConfig;
use rdkafka::message::{Header, OwnedHeaders};
use rdkafka::producer::{FutureProducer, FutureRecord};
use rdkafka::util::Timeout;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let producer: FutureProducer = ClientConfig::new()
.set("bootstrap.servers", "localhost:9092")
.set("acks", "all")
.set("enable.idempotence", "true") // requires acks=all; the client enforces this
.set("linger.ms", "50")
.set("batch.size", "65536")
.create()?;
let headers = OwnedHeaders::new().insert(Header { key: "source", value: Some("demo") });
let record = FutureRecord::to("orders")
.key("order-42")
.payload("hello kafka")
.headers(headers);
match producer.send(record, Timeout::After(Duration::from_secs(10))).await {
Ok(delivery) => println!("produced to partition={} offset={}", delivery.partition, delivery.offset),
Err((err, _)) => return Err(Box::new(err)),
}
Ok(())
}Error quick reference
A produce can fail for reasons unrelated to the settings above — most commonly an authorization deny or an oversized record:
| Trigger | Result |
|---|---|
| A produced record (or the assembled batch) exceeds the message-size ceiling | MESSAGE_TOO_LARGE |
acks=0 sent to a follower on a multi-node cluster | Dropped silently — no redirect signal; see the durability Callout above |
| Producing without the required Write ACL grant on the topic | TOPIC_AUTHORIZATION_FAILED |
The full error-code and numeric-limit tables live in Limits & rules.
Related
Durability & Retention
The acks 0/1/all contract, quorum-fsynced zero-acked-loss on the next engine, and retention by time and size.
Partitions & Ordering
Client-side key hashing (murmur2 vs CRC32), partition counts, and the per-partition ordering guarantee.
Limits & rules
The full numeric-limit table — partitions, message size, connections, and fan-out caps.
Consuming
Subscribe with a consumer group, commit offsets, and seek by offset or timestamp.
Was this page helpful?
Getting Started
Enable the Kafka connector, then produce and consume your first message over the wire protocol with kcat and seven client libraries — a full round-trip.
Consuming
Consume from KubeMQ over the Kafka protocol — join a consumer group, commit offsets manually or automatically, and seek by offset or timestamp.