How to Build an Exactly-Once Kafka Pipeline
Kafka guarantees at-least-once delivery, so duplicates are a fact of life - even with idempotent producers. Exactly-once processing for downstream effects is the consumer's job, built with a dedup guard around each write.
Why Kafka delivers duplicates
Kafka's default delivery semantics are at-least-once: a producer retries on
network failures, and the broker may deliver the same message more than once.
Even with enable.idempotence=True on the producer (which prevents broker-side
duplicates), application retries and redeliveries can still send the same logical
event twice. The consumer is responsible for making its downstream effects
idempotent.
The producer: simulate a duplicate publish
import json
from kafka import KafkaProducer
BROKER = "localhost:9092"
TOPIC = "orders"
def main():
p = KafkaProducer(
bootstrap_servers=BROKER,
value_serializer=lambda v: json.dumps(v).encode(),
)
# Send each order_id twice - simulating a retry/redelivery
for _ in range(2):
for order_id in range(1, 101):
p.send(TOPIC, {"order_id": order_id})
p.flush()
p.close()
200 messages land in the topic (100 unique order IDs, each sent twice).
The consumer: dedup with a seen-set
Track processed IDs in an in-memory set. Skip any message whose key has already been handled:
import json
from kafka import KafkaConsumer
BROKER = "localhost:9092"
TOPIC = "orders"
def main():
consumer = KafkaConsumer(
TOPIC,
bootstrap_servers=BROKER,
auto_offset_reset="earliest",
consumer_timeout_ms=5000,
value_deserializer=lambda b: json.loads(b.decode()),
)
seen = set()
with open("processed.txt", "w") as f:
for msg in consumer:
oid = msg.value["order_id"]
if oid in seen:
continue # duplicate - skip
seen.add(oid)
f.write(f"{oid}\n") # downstream effect fires exactly once
consumer.close()
processed.txt ends up with exactly 100 lines despite 200 messages in the
topic. The if oid in seen: continue guard is the entire exactly-once
mechanism for downstream effects.
What makes this production-ready
The in-memory set works for a single consumer session, but restarts lose the state. Production setups persist the seen-IDs to survive crashes and restarts:
- Redis
SETNX- atomic "set if not exists"; returns 0 on a duplicate so you skip in one round-trip. - Postgres
INSERT ... ON CONFLICT DO NOTHING- idempotent upsert; the row already existing is the dedup signal. - Kafka Streams
RocksDB- persistent state store that survives consumer restarts inside the same consumer group.
Pair the dedup store with consumer-group offset management so the consumer resumes from its last committed offset on restart rather than re-reading the whole topic. Those two pieces together - persistent seen-set plus committed offsets - give you reliable exactly-once semantics for any downstream effect.
Check your work
wc -l processed.txt # should print 100
sort -u processed.txt | wc -l # should also print 100 - no duplicates
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Building a KafkaProducer that simulates duplicate message delivery
- Writing an idempotent consumer with a seen-set dedup guard
- Verifying exactly 100 unique records despite 200 messages in the topic
FAQ
Does Kafka guarantee exactly-once delivery?
Kafka offers exactly-once semantics (EOS) within the broker using idempotent producers and transactional writes. But downstream effects - database writes, HTTP calls, file writes - outside the Kafka transaction still require a consumer-side dedup guard; EOS alone does not cover them.
How do I deduplicate Kafka messages in Python?
Track processed message keys in a set (or a persistent store like Redis or Postgres). On each message, check if the key is already in the set - if so, skip it; if not, add it and process. This makes downstream effects idempotent regardless of how many times the message is delivered.
What is the difference between at-least-once and exactly-once in Kafka?
At-least-once means every message is delivered, but duplicates may occur on retries. Exactly-once means each logical event triggers its downstream effect exactly one time - achieved by combining Kafka's idempotent producer with a consumer-side dedup store and committed offsets.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →