Every broker and every HTTP client retries. A webhook times out, the sender tries again, and your pipeline sees the same event twice. If the event is a payment, a usage record or an inventory change, “twice” is a bug in someone’s invoice.
Kafka’s exactly-once semantics cover one hop: producer to broker to a consumer that writes back to Kafka. The moment your consumer writes to Postgres, calls an API or updates a cache, you are outside that guarantee. What you can build is at-least-once delivery with idempotent effects, and that gives you the same result.
The pattern: a dedup key and one transaction
Give every event a key that identifies the fact, not the delivery. A UUID generated by the producer at the moment the thing happened works. A hash of the delivery attempt does not.
Then make “have I seen this?” and “apply the effect” a single atomic step:
BEGIN;
INSERT INTO processed_events (event_id, processed_at)
VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING;
-- only continue if a row was inserted
UPDATE usage_totals
SET units = units + $2
WHERE customer_id = $3;
COMMIT;
If the insert conflicts, skip the update and commit. A replay of last Tuesday’s topic then changes nothing.
Three ways this goes wrong
- The key is generated by the consumer. Each retry produces a new key, so nothing is ever deduplicated.
- The check and the effect are separate transactions. A crash between them leaves you either double-applying or never applying.
- The dedup table grows forever. Keep keys for longer than your maximum replay window, then expire them by partition.
Replay becomes a feature
Once effects are idempotent, you can rewind a consumer group to fix a bug in your transform and let it run. No one-off scripts, no “did we already fix that day?” spreadsheets. This is the same pattern that keeps the event pipeline in ABAXUS, our usage-based billing engine, accurate under retries.
If your pipeline can’t survive a replay today, that is the first thing we would look at in an architecture review.
More from the blog
Batch to streaming without freezing your roadmap
A phased plan for moving nightly jobs onto streaming pipelines, with parallel runs and a cutover you can roll back.
Hire, freelancer, or dedicated pod: how to decide
Three ways for European companies to add data engineering capacity, what each really costs, and five questions that tell you which one fits.
Kafka consumer lag: the first alert to set up, and how to read it
Lag tells you whether consumers keep up. Alerting on its trend, not its size, catches stuck consumers before customers do.