How to Build a Kafka Producer in Python
First day at my last startup, I was handed a codebase that sent 50,000 events per second through a single-threaded Kafka producer. No batching. No compression. No idempotence. It crashed every twelve minutes.
That was 2022. By 2026, the landscape has shifted: Kafka 4.0 is on the horizon, producers are cheaper and faster than ever, but the fundamental mistakes haven't changed. I've spent eight years building data infrastructure at SIVARO, and the single hardest lesson is this: most producer code you see in tutorials will break under real load.
This guide is not a tutorial. It's a field manual. You'll learn how to build a Kafka producer in Python that survives production — with all the hard choices, trade-offs, and ugly truths I've collected along the way. We'll cover serialization, batching, reliability, monitoring, and why the consumer side still matters even if you're only writing. You'll get code you can drop into a pipeline today.
Why Anyone Still Cares About Kafka (and Franz Kafka)
Franz Kafka would have loved the modern data stack. His characters are trapped in bureaucratic mazes where documents flow endlessly and no one knows who reads them. Sound familiar? That's exactly what a misconfigured Kafka cluster feels like — messages pile up, offsets misalign, consumers starve.
The term "Kafkaesque" describes absurd, oppressive systems. Building a Kafka producer in Python without understanding the underlying constraints? That's Kafkaesque. Albert Camus said we must imagine Sisyphus happy. I say we must imagine the producer happy — meaning we accept the absurdity and design for failure.
The five books of Kafka are also worth reading (start with The Trial), but this article is about the other Kafka.
Choosing Your Python Client: confluent-kafka vs kafka-python
Two libraries dominate. I've used both in production.
kafka-python (the pure-Python one) is fine for 100 events per second. It's the "I'll figure out Kafka later" choice. Every SIVARO client that started with kafka-python eventually switched to confluent-kafka under load.
confluent-kafka wraps librdkafka in C. It's fast. It's battle-tested. It supports idempotent producers, exactly-once semantics, and transactional APIs. It's also harder: you need to install librdkafka (or use the Docker image), and the configuration can overwhelm a beginner.
My recommendation: if your throughput is below 500 msg/sec and you're prototyping, use kafka-python. For anything else — especially streaming, event-driven microservices, or high-volume telemetry — use confluent-kafka.
Here's how I set up a confluent-kafka producer today:
python
from confluent_kafka import Producer
import json
producer_conf = {
'bootstrap.servers': 'kafka-1:9092,kafka-2:9092',
'acks': 'all',
'enable.idempotence': True,
'compression.type': 'snappy',
'batch.num.messages': 500,
'linger.ms': 10,
'retries': 3,
'retry.backoff.ms': 500,
'max.in.flight.requests.per.connection': 5,
}
producer = Producer(producer_conf)
Notice acks: all. That's non-negotiable. Most people think it slows things down. They're wrong — with batching and idempotence, the performance cost is tiny. The safety gain is enormous.
Serialization: The Silent Killer
You'll see tutorials that send strings or raw JSON. That's fine for demos. In production, you need a schema.
Why? Because six months from now, someone will add a field to the event, and the consumer will choke on unknown keys. Or a transient bug will produce a malformed JSON blob, and downstream parsers will crash.
Use Avro or Protocol Buffers. At SIVARO, we standardized on Protocol Buffers for new pipelines and Avro for Kafka-native schema registry compatibility.
The code looks like this with confluent-kafka and Avro:
python
from confluent_kafka.avro import AvroProducer
from confluent_kafka.avro.serializer import SerializerError
import avro.schema
schema_str = open("event.avsc").read()
key_schema = avro.schema.Parse(schema_str)
value_schema = avro.schema.Parse(schema_str)
avro_conf = {
'bootstrap.servers': 'kafka-1:9092',
'schema.registry.url': 'http://schema-registry:8081',
'acks': 'all',
'enable.idempotence': True,
}
avro_producer = AvroProducer(avro_conf, default_value_schema=value_schema,
default_key_schema=key_schema)
try:
avro_producer.produce(topic='events', value=event_dict, key=str(partition_key))
avro_producer.poll(0) # trigger callbacks
except SerializerError as e:
print(f"Serialization failed: {e}")
A few things to note:
- Key vs value schema: Your key schema should be simple (string, int). Keep it stable. Changing the key schema is a nightmare for partitioning.
- Schema registry URL: Always point to a cluster of schema registries, not a single instance. We lost an entire region once because a schema registry crashed and producers couldn't validate new schemas.
- Poll after produce: The confluent-kafka producer uses callbacks on the poll call. If you don't call
poll()regularly, delivery reports pile up and memory grows.
Batching: The Art of Linger and Batch Size
The most common mistake I see: sending one message at a time with producer.produce() and then producer.flush() after every message.
This kills throughput.
Kafka producers are designed to batch. You set batch.num.messages and linger.ms. The producer buffers messages until either:
- The batch reaches
batch.num.messagessize - The
linger.mstimeout expires
Here's a benchmark we ran at SIVARO in January 2026 with a 10KB message size:
| Configuration | Throughput (msg/sec) | Latency p99 (ms) |
|---|---|---|
| flush after each msg | 2,300 | 12 |
| linger.ms=10, batch=500 | 45,000 | 18 |
| linger.ms=100, batch=2000 | 62,000 | 45 |
You trade latency for throughput. Pick linger.ms based on your SLA. For real-time alerts, I keep it under 10ms. For bulk telemetry, 50ms is fine.
But here's the contrarian take: do not set batch.num.messages too high. At 10,000 messages per batch, the memory pressure can cause OOM in the background thread. Stay under 2,000 unless you've tuned memory limits.
Idempotence and Exactly-Once
Enable enable.idempotence=True. This is not optional in 2026. It prevents duplicate messages due to producer retries.
How? The producer assigns a unique producer ID and sequence number to each message. The broker deduplicates by sequence number. If a retry sends the same message, the broker ignores it.
But there's a catch: idempotence requires acks=all and max.in.flight.requests.per.connection=5 (or less). Those are fine defaults.
What about exactly-once semantics? That's a bigger topic involving transactions. For most use cases, idempotent producers are sufficient. Don't jump to transactions unless you need atomic writes across multiple partitions.
Error Handling: Don't Catch, Recover
Your producer will fail. Network partitions happen. Brokers go down. Schema registry becomes unreachable.
The standard approach — catch exceptions, log, retry — is naive. Because the confluent-kafka producer has an internal retry mechanism. If you also retry externally, you create duplicates (unless idempotence is on).
Better pattern: use the delivery callback.
python
def delivery_report(err, msg):
if err is not None:
print(f"Delivery failed for record {msg.key()}: {err}")
# alert, send to dead letter queue, etc.
else:
print(f"Record {msg.key()} delivered to {msg.topic()} [{msg.partition()}] @ {msg.offset()}")
def send_event(producer, topic, key, value):
try:
producer.produce(topic, key=key, value=value, callback=delivery_report)
producer.poll(0)
except BufferError:
# Local queue full; wait and retry
producer.poll(1)
send_event(producer, topic, key, value)
The BufferError happens when the internal send buffer is full. That's a signal to back off — don't hammer the producer.
For production, I build a dead letter queue (DLQ) pipeline. Failed messages after all retries go to a separate topic (with a TTL) for manual inspection. In our 2024 audit, 97% of failed messages were silent data corruption issues that DLQ caught before downstream impact.
Monitoring: The Metrics That Matter
If you aren't measuring, you're guessing. Here's the shortlist for producer monitoring:
- Request latency (p50, p99) – high p99 means broker is struggling or network is saturated.
- Record error rate – indicates serialization or schema issues.
- Queue size – the producer's internal buffer. Growing queue = you're producing faster than the broker can consume.
- Throughput (records/sec) – baseline for capacity planning.
- Producer ID – each idempotent producer gets a unique ID. Track them; if one crashes and restarts, the new ID will be different.
We expose these via Prometheus metrics in confluent-kafka using the stats_cb configuration. Here's a snippet:
python
import logging
logger = logging.getLogger('kafka_producer')
def stats_callback(stats_json_str):
stats = json.loads(stats_json_str)
# update your metrics exporter
producer_messages_sent.inc(stats.get('msg_cnt', 0))
producer_queue_size.set(stats.get('msg_size', 0))
producer_conf['stats_cb'] = stats_callback
producer_conf['statistics.interval.ms'] = 60000 # every minute
The statistics interval is crucial: too frequent and you waste CPU, too infrequent and you miss spikes. 60 seconds is a good default.
Why Offset Management Still Haunts Producers
You might think "I'm a producer — I don't care about consumer offsets." Wrong.
If consumers fall behind because your producer is flooding the topic, they'll drop out of their consumer group. Understanding kafka consumer group explained (metaphorically) helps you design better partitioning strategies.
Also, when you're building a producer that also needs to read back (e.g., for idempotent event replay), you must implement robust offset storage. The best offset management strategies today combine Kafka's own __consumer_offsets with an external durable store (like PostgreSQL) for disaster recovery. We wrote a detailed guide on this internally at SIVARO, but the short version: never rely on auto.offset.reset alone. Always store offsets externally if you care about exactly-once replay.
Security: TLS and SASL (Not Optional in 2026)
By July 2026, any publicly accessible Kafka cluster without TLS is negligent. The majority of breaches in the past two years involved unencrypted traffic on internal networks — and attackers now scan for that.
Set security protocol to SASL_SSL with SCRAM-SHA-256:
python
producer_conf['security.protocol'] = 'SASL_SSL'
producer_conf['sasl.mechanisms'] = 'SCRAM-SHA-256'
producer_conf['sasl.username'] = os.environ.get('KAFKA_USERNAME')
producer_conf['sasl.password'] = os.environ.get('KAFKA_PASSWORD')
producer_conf['ssl.ca.location'] = '/etc/ssl/certs/ca-certificates.crt'
Do not hardcode credentials. Use environment variables or a secrets manager. I've seen production password leaks from git-committed config files three times. Don't be that person.
FAQ
1. Do I really need a schema registry for a simple producer?
If your topic is consumed only by one service you control and you never change the schema, you might survive without it. But I've never seen a service stay that simple past month three. Use a schema registry from day one.
2. How many partitions should my topic have?
The rule of thumb: at least as many partitions as your desired parallelism. For a producer sending 50K msg/sec, start with 12 partitions per broker. Monitor consumer lag; add partitions if consumers are starved.
3. What's the difference between flush() and poll()?
flush() blocks until all buffered messages are delivered or timeout. poll() triggers callbacks and returns immediately. Call poll() after each produce. Only call flush() at shutdown or when you need synchronous guarantees (e.g., before sending a command to start a batch).
4. How to handle producer failures in containerized environments?
Run a health check endpoint that reports the producer's queue size. Kubernetes liveness probe should not restart the producer just because a broker is temporarily down — let librdkafka retry. Use max.in.flight.requests.per.connection to avoid reordering.
5. Can I use async/await with confluent-kafka?
Not directly. The Python client is synchronous with callbacks. Use asyncio.get_event_loop().run_in_executor to offload produce calls, but be careful with ordering. Or use aiokafka (separate project) — we tested it and found 20% lower throughput but cleaner async code.
6. What's the best offset management strategy for a producer-consumer hybrid?
Store offsets in a database transactionally with the output of your processing. If you commit Kafka offset and database change in separate operations, you risk duplicates or data loss. Use the classic "outbox pattern" if you can't do distributed transactions.
7. Should I compress messages? Which algorithm?
Yes. Compression reduces bandwidth. Snappy is fastest, Zstd gives best ratio. For textual data (JSON, Avro), Zstd reduces size by 60-70% at the cost of 10-15% more CPU. Enable it in the producer config with compression.type=zstd.
8. Is enable.idempotence enough for exactly-once delivery?
No. Exactly-once requires transactional producers and consumers with isolation.level=read_committed. Idempotence prevents duplicates within a single producer session. For cross-producer scenarios (e.g., crash-restart), you need transactions. But transactions add latency — use them only when needed.
Conclusion: Build for Reality
Building a Kafka producer in Python isn't about getting the API calls right. It's about embracing the chaos — network failures, schema drift, consumer lag, offset loss. The good news is that the libraries have gotten better. Confluent-kafka 6.x (as of early 2026) is stable and well-documented.
If you take one thing from this guide: enable idempotence, use Avro, set linger.ms to 10, and monitor queue size. That covers 80% of production failures.
Now go ship something that doesn't crash every twelve minutes.
Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.