KubeMQ
ConnectorsKafkaHow-to guides

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=65536
package 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:

TriggerResult
A produced record (or the assembled batch) exceeds the message-size ceilingMESSAGE_TOO_LARGE
acks=0 sent to a follower on a multi-node clusterDropped silently — no redirect signal; see the durability Callout above
Producing without the required Write ACL grant on the topicTOPIC_AUTHORIZATION_FAILED

The full error-code and numeric-limit tables live in Limits & rules.

Was this page helpful?

On this page