Kafka
Point unmodified Kafka clients at KubeMQ over the native wire protocol — produce, consume, consumer groups, and compaction, with no client-library swap.
Point your Kafka clients at KubeMQ by repointing bootstrap.servers — no client-library
swap, no code change. The Kafka connector is a built-in, wire-protocol bridge inside
kubemq-server that speaks the real Kafka request/response protocol — length-prefixed
frames, flexible versions, the kmsg codec — over plain TCP and TLS. Any off-the-shelf
Kafka client — Java kafka-clients/AdminClient/Spring, librdkafka/kcat/confluent-kafka,
or franz-go/sarama/segmentio — connects to KubeMQ exactly as it would to a real Kafka
broker.
What is the Kafka connector
The Kafka connector implements Kafka's Produce/Fetch/group-coordinator/admin request
surface directly inside kubemq-server — there's no separate broker process to run and no
protocol-translation layer for your application to configure around. Every produced record
lands in a persistent, ordered, replayable Events Store channel whose Sequence
maps one-to-one onto a Kafka offset: durable, restart-stable, and identical across every
node of a cluster.
Key capabilities:
- Native produce/consume with classic consumer groups — the large majority of everyday Kafka usage, a straight repoint with no client changes.
- OAUTHBEARER/OIDC federated auth, alongside SASL/PLAIN, SASL/SCRAM, and mTLS client certificates. Rebuild Kafka ACLs as KubeMQ authorization rules before cutover; existing Kafka ACLs are not imported.
- Compacted topics (
cleanup.policy=compact), which unlock the compaction-dependent ecosystem — Kafka Connect and Kafka Streams both run against it. - Transactions and exactly-once semantics (EOS) — a transactional producer with
read_committedisolation and producer fencing. - Static membership (
group.instance.id, KIP-345), so a consumer restart rejoins its group without triggering a rebalance.
Before moving a production workload, review What differs from Kafka.
On by default. A stock kubemq-server binds port 9092 out of the box (9093 once TLS
is configured) — nothing to enable. To turn the connector off, set
CONNECTORS_KAFKA_ENABLE=false (Docker) or spec.kafka.enabled: false (Kubernetes). If
9092 is already taken on the host — a real Kafka broker running locally, say — the server
logs an error and keeps running without the Kafka connector, even though the dashboard still
lists it as enabled. See Getting started.
How it works
A Kafka client dials the connector's TCP listener and issues the same request types it
would send to a real broker. A Produce request is appended to the topic's Events Store
log; a Fetch request — including one driven by a consumer group's assigned partitions —
reads back from that same log. Because each Kafka offset maps one-to-one onto the log's durable Sequence, both
directions stay durable and ordered through the message broker underneath.
A Produce request from a Kafka client is appended to the topic's Events Store log kafka.{topic} through the message broker; a Fetch request — including one driven by a consumer group's assigned partitions — reads back from that same log, so each Kafka offset maps one-to-one onto the log's durable Sequence.
Runs on the auto-selected next storage engine. Kafka's headline capabilities —
compacted topics and the quorum-fsynced ack contract — exist only on KubeMQ's next
storage engine. On a fresh store, Kafka (on by default) auto-selects next with no
manual store.engine step. See
Storage Engines → Zero-config engine selection
for the full selection rules, including what happens on an existing store.
Ports & protocol surface
| Port | Transport | Protocol | Notes |
|---|---|---|---|
9092 | Plain TCP | Kafka wire protocol — PLAINTEXT / SASL_PLAINTEXT | Default listener. On by default — bound unless CONNECTORS_KAFKA_ENABLE=false. Disable just this listener by setting CONNECTORS_KAFKA_PORT="". Must differ from the TLS port. |
9093 | TLS over TCP | Kafka wire protocol — SSL / SASL_SSL | Reuses the server-wide Security block (cert + key) — there is no Kafka-specific TLS option. Required for OAUTHBEARER (refused on plaintext). |
Clients read one setting, bootstrap.servers (or the client's equivalent), pointed at
localhost:9092 for the plaintext listener. See
Configuration for the enable flag, the
advertised-host/port pair, and where the full field-by-field settings reference lives.
Send & receive a message
The example below produces one message to the orders topic and reads it back on
localhost:9092 — the only change versus a real Kafka client is the
bootstrap.servers value. No KubeMQ SDK, no code change: every tab uses a stock Kafka
client library.
# Produce one message to topic "orders"
echo "hello from kcat" | kcat -P -b localhost:9092 -t orders
# Consume from the beginning and exit after one message
kcat -C -b localhost:9092 -t orders -o beginning -c 1package main
import (
"context"
"fmt"
"log"
"github.com/twmb/franz-go/pkg/kgo"
)
func main() {
ctx := context.Background()
// bootstrap.servers repoint only — no KubeMQ SDK, no code change.
cl, err := kgo.NewClient(
kgo.SeedBrokers("localhost:9092"),
kgo.ConsumeTopics("orders"),
kgo.ConsumeResetOffset(kgo.NewOffset().AtStart()),
)
if err != nil {
log.Fatal(err)
}
defer cl.Close()
record := &kgo.Record{Topic: "orders", Value: []byte("hello from franz-go")}
if err := cl.ProduceSync(ctx, record).FirstErr(); err != nil {
log.Fatalf("produce: %v", err)
}
fmt.Println("produced to orders")
fetches := cl.PollFetches(ctx)
fetches.EachRecord(func(r *kgo.Record) {
fmt.Printf("received: %s\n", string(r.Value))
})
}from confluent_kafka import Consumer, Producer
BOOTSTRAP = "localhost:9092"
def main() -> None:
producer = Producer({"bootstrap.servers": BOOTSTRAP})
producer.produce("orders", value=b"hello from confluent-kafka")
producer.flush()
print("produced to orders")
consumer = Consumer({
"bootstrap.servers": BOOTSTRAP,
"group.id": "orders-consumer",
"auto.offset.reset": "earliest",
})
consumer.subscribe(["orders"])
msg = consumer.poll(timeout=10.0)
if msg is not None and msg.error() is None:
print(f"received: {msg.value().decode()!r}")
consumer.close()
if __name__ == "__main__":
main()import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public final class Main {
public static void main(String[] args) {
String bootstrap = "localhost:9092";
Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", bootstrap);
producerProps.put("key.serializer", StringSerializer.class.getName());
producerProps.put("value.serializer", StringSerializer.class.getName());
try (KafkaProducer<String, String> producer = new KafkaProducer<>(producerProps)) {
producer.send(new ProducerRecord<>("orders", "hello from kafka-clients"));
producer.flush();
System.out.println("produced to orders");
}
Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", bootstrap);
consumerProps.put("group.id", "orders-consumer");
consumerProps.put("key.deserializer", StringDeserializer.class.getName());
consumerProps.put("value.deserializer", StringDeserializer.class.getName());
consumerProps.put("auto.offset.reset", "earliest");
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps)) {
consumer.subscribe(Collections.singletonList("orders"));
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(10));
for (ConsumerRecord<String, String> record : records) {
System.out.printf("received: %s%n", record.value());
}
}
}
}import { Kafka } from "kafkajs";
const kafka = new Kafka({
clientId: "kubemq-kafka-example",
brokers: ["localhost:9092"],
});
async function main(): Promise<void> {
const producer = kafka.producer();
await producer.connect();
await producer.send({
topic: "orders",
messages: [{ value: "hello from kafkajs" }],
});
console.log("produced to orders");
await producer.disconnect();
const consumer = kafka.consumer({ groupId: "orders-consumer" });
await consumer.connect();
await consumer.subscribe({ topic: "orders", fromBeginning: true });
await consumer.run({
eachMessage: async ({ message }) => {
console.log(`received: ${message.value?.toString()}`);
await consumer.disconnect();
process.exit(0);
},
});
}
main().catch((err) => {
console.error(err);
process.exit(1);
});using Confluent.Kafka;
const string bootstrapServers = "localhost:9092";
const string topic = "orders";
var producerConfig = new ProducerConfig { BootstrapServers = bootstrapServers };
using (var producer = new ProducerBuilder<Null, string>(producerConfig).Build())
{
await producer.ProduceAsync(topic, new Message<Null, string> { Value = "hello from Confluent.Kafka" });
Console.WriteLine("produced to orders");
}
var consumerConfig = new ConsumerConfig
{
BootstrapServers = bootstrapServers,
GroupId = "orders-consumer",
AutoOffsetReset = AutoOffsetReset.Earliest,
};
using var consumer = new ConsumerBuilder<Ignore, string>(consumerConfig).Build();
consumer.Subscribe(topic);
var result = consumer.Consume(TimeSpan.FromSeconds(10));
if (result is not null)
Console.WriteLine($"received: {result.Message.Value}");
consumer.Close();require "rdkafka"
BOOTSTRAP = "localhost:9092"
producer_config = Rdkafka::Config.new("bootstrap.servers" => BOOTSTRAP)
producer = producer_config.producer
producer.produce(topic: "orders", payload: "hello from rdkafka-ruby").wait
puts "produced to orders"
producer.close
consumer_config = Rdkafka::Config.new(
"bootstrap.servers" => BOOTSTRAP,
"group.id" => "orders-consumer",
"auto.offset.reset" => "earliest"
)
consumer = consumer_config.consumer
consumer.subscribe("orders")
consumer.each do |message|
puts "received: #{message.payload}"
break
end
consumer.closeuse rdkafka::config::ClientConfig;
use rdkafka::consumer::{BaseConsumer, Consumer};
use rdkafka::message::Message;
use rdkafka::producer::{BaseProducer, BaseRecord, Producer};
use std::time::Duration;
fn main() {
let bootstrap = "localhost:9092";
let producer: BaseProducer = ClientConfig::new()
.set("bootstrap.servers", bootstrap)
.create()
.expect("producer creation failed");
producer
.send(
BaseRecord::to("orders")
.payload("hello from rust-rdkafka")
.key("order-1"),
)
.expect("produce failed");
producer.flush(Duration::from_secs(10)).expect("flush failed");
println!("produced to orders");
let consumer: BaseConsumer = ClientConfig::new()
.set("bootstrap.servers", bootstrap)
.set("group.id", "orders-consumer")
.set("auto.offset.reset", "earliest")
.create()
.expect("consumer creation failed");
consumer.subscribe(&["orders"]).expect("subscribe failed");
if let Some(result) = consumer.poll(Duration::from_secs(10)) {
let msg = result.expect("poll failed");
if let Some(payload) = msg.payload() {
println!("received: {}", String::from_utf8_lossy(payload));
}
}
}Supported clients
The connector speaks the genuine Kafka wire protocol, so any conformant client library works — there is no KubeMQ SDK and no proto bindings. The examples above pin one client per language; alternates are noted where the ecosystem commonly uses more than one.
| Language | Client library | Notes |
|---|---|---|
| kcat | kcat (librdkafka CLI) | The canonical quick-check Kafka client — no code, just a bootstrap address. |
| Go | github.com/twmb/franz-go | Matches the connector's own conformance-harness client; sarama and segmentio/kafka-go also work. |
| Python | confluent-kafka (librdkafka bindings) | |
| Java | org.apache.kafka:kafka-clients | AdminClient and Spring Kafka run on top of the same client unchanged. |
| JavaScript / TypeScript | kafkajs | |
| C# / .NET | Confluent.Kafka (librdkafka bindings) | |
| Ruby | rdkafka (librdkafka bindings) | |
| Rust | rdkafka (librdkafka bindings) |
Share groups (KIP-932) are generally available. Queue-style acquire/acknowledge
consumption is certified with Java's KafkaShareConsumer (kafka-clients 4.3) and the Kafka
4.3 console tools on a single node and on three-node clusters, including a killed share
coordinator with nothing lost and nothing accepted twice. franz-go works too; librdkafka's
share consumer is still an upstream preview. A few behaviors deliberately differ from Apache
Kafka — see Share Groups and
the share-group differences.
Not every capability above is proven to the same tier — see the
fitness matrix for the full
supported / caveat / roadmap / unsupported breakdown before you commit to a migration. Every
Kafka setting — ports, advertised host/port, SASL mechanisms, OAUTHBEARER, and the advanced
tuning knobs — is documented field-by-field in
the Kafka settings reference. Moving an
existing Kafka, MSK, or Confluent workload onto KubeMQ starts with the read-only
kmq assess kafka command, then the kmq migrate tool —
Migrate from Kafka walks through the
full assess → replicate → translate → cutover playbook, and the full
kmq CLI reference documents both commands.
Next steps
Getting started
Start kubemq-server, then produce and consume your first message over the native wire protocol with kcat and seven client libraries — a full round-trip in minutes.
Configuration
How the Kafka connector is enabled, ported, and secured — the on-by-default CONNECTORS_KAFKA_ENABLE flag, the 9092/9093 listeners, and where the full field-by-field settings reference lives.
Producing
Produce to KubeMQ over the Kafka protocol — message keys and headers, the acks durability setting, batching, and the idempotent producer, across kcat and seven client libraries.
Consuming
Consume from KubeMQ over the Kafka protocol — subscribing with a consumer group, committing offsets manually or automatically, and seeking by offset or timestamp.
Was this page helpful?