Delivery guarantees and effectively-once
In one line
Networks force a choice between possibly losing messages (at-most-once) and possibly duplicating them (at-least-once); real systems choose at-least-once and make processing idempotent, which gives 'effectively-once' results end to end.
What it is
A delivery guarantee describes what happens to a message when something fails between sender and receiver. At-most-once: send and forget; a crash can lose the message, but it is never processed twice. At-least-once: retry until acknowledged; nothing is lost, but a lost acknowledgement causes a duplicate. Exactly-once: each message affects the result exactly one time.
Exactly-once delivery over an unreliable network is impossible in general: if the sender does not get an acknowledgement, it cannot know whether the message or the acknowledgement was lost. What is achievable is exactly-once effect (often called effectively-once): deliver at least once, and make the receiver's processing idempotent so a duplicate changes nothing.
Kafka's 'exactly-once semantics' (EOS) is real but scoped. The idempotent producer removes duplicates caused by producer retries within a partition, and transactions let a consume-transform-produce job atomically write output records and commit input offsets. Consumers using isolation.level=read_committed then never see aborted writes. This is exactly-once inside Kafka. The moment your consumer charges a card, sends an SMS or writes to Postgres, Kafka cannot roll that back, so you are back to at-least-once plus idempotency.
Idempotency is therefore the core skill: give every message or request a unique id, and make the side effect conditional on that id not having been applied (a dedupe table in the same transaction, a unique constraint, an upsert keyed by a natural id, or a version check).
Why interviewers ask
Payments, wallets, inventory, notifications and ledgers all break on duplicates or losses: a double debit, a missed refund, two OTP SMS, overselling. Interviewers love asking 'how do you make sure the user is charged exactly once?' and listening for whether you claim magic exactly-once or explain at-least-once with idempotency keys. Confusing Kafka's internal exactly-once with end-to-end exactly-once is a classic red flag.
How it works
- 1Producer side: retry sends on timeout, with a stable message id generated once (UUID or business id), so retries carry the same id.
- 2Broker side: persist and replicate before acknowledging (Kafka acks=all with min.insync.replicas=2; SQS and RabbitMQ durable queues with publisher confirms). Enable the idempotent producer in Kafka so broker-level retries do not duplicate records.
- 3Consumer side: process first, then acknowledge or commit the offset. A crash in between causes redelivery, never loss.
- 4Make processing idempotent: in the same database transaction as the side effect, insert the event id into a processed_events table with a unique key. If the insert conflicts, skip the side effect.
- 5Where possible use naturally idempotent operations: SET status = 'shipped' WHERE order_id = 7 AND status = 'packed' (a state transition), an upsert by business key, or 'set balance to X at version V' with optimistic concurrency, rather than 'add 100'.
- 6For external calls (payment gateways, SMS, email), pass an idempotency key the provider honours (Stripe's Idempotency-Key header, a merchant order id for UPI or card payments). If the provider has no such key, record intent before calling and reconcile uncertain outcomes by querying status.
- 7For pure Kafka-to-Kafka stream processing, use Kafka transactions (Kafka Streams processing.guarantee=exactly_once_v2, or Flink's two-phase-commit Kafka sink) so output and offsets commit atomically.
- 8Bound the dedupe store: keep processed ids for longer than the maximum redelivery window (for example 7 days, matching topic retention), then expire them.
Worked example · Cashback credit to a wallet, delivered twice
- A payments service publishes PaymentSucceeded {event_id: e-77, user_id: 42, cashback: 50} to Kafka. The rewards consumer credits Rs 50 to user 42's wallet.
- The consumer applies the credit and is killed by a deploy before committing its offset. On restart, it reads e-77 again.
- Naive consumer: balance goes from 100 to 150, then to 200. The user got Rs 100 cashback; across 1M events a day, even a 0.1% duplicate rate is 1,000 wrong credits daily.
- Idempotent consumer: inside one Postgres transaction it inserts (rewards, e-77) into processed_events and updates the balance. The first run commits both. The second run's insert hits the unique key, so it skips the update. Balance stays 150.
- The dedupe table grows by 1M rows a day, about 60 MB with ids and indexes; a daily job deletes rows older than 7 days, so it stays around 7M rows.
- Result: at-least-once delivery from Kafka plus idempotent processing gives an exactly-once effect on the wallet.
Picture it
Sending an important courier with tracking: if you get no delivery confirmation, you send it again, so the receiver might get two copies. Exactly-once is achieved not by the courier but by the receiver: the cheque has a unique number, and the bank will never cash the same cheque number twice.
Reference code
def handle(event, db):
# One local transaction: dedupe record + side effect commit together.
with db.transaction():
inserted = db.execute(
"INSERT INTO processed_events (consumer, event_id) VALUES (%s, %s) "
"ON CONFLICT DO NOTHING",
("wallet-credit", event["event_id"]),
).rowcount
if inserted == 0:
return # duplicate delivery: already applied, do nothing
db.execute(
"UPDATE wallets SET balance = balance + %s WHERE user_id = %s",
(event["amount"], event["user_id"]),
)
# Commit the Kafka offset only after the DB transaction succeeded.
# A crash before this line means redelivery, which the dedupe row absorbs.Key patterns
Dedupe table in the same transaction
When: Consumers whose side effect is a database write
How: processed_events(consumer, event_id) with a unique key, inserted in the same transaction as the business write; skip if it already exists.
Idempotency keys on APIs
When: Clients retry POST requests (payments, orders, bookings)
How: Client sends a unique key; server stores key, request hash and response; a repeat returns the stored response; a different request with the same key is rejected.
Conditional state transitions
When: Entities with a status lifecycle
How: UPDATE ... SET status = 'paid' WHERE id = ? AND status = 'pending'; zero rows updated means already done or invalid.
Transactional consume-transform-produce
When: Kafka-to-Kafka stream processing
How: Kafka transactions or Kafka Streams exactly_once_v2: output records and input offsets commit atomically; downstream reads with read_committed.
Reconciliation
When: Calls to external systems with uncertain outcomes
How: Record intent first, query the provider for status on timeout, and run periodic comparisons to fix drift.
Real-world use
Stripe idempotency keys
Stripe's API accepts an Idempotency-Key header on POST requests. A retried request with the same key returns the stored result of the first attempt instead of creating a second charge. Keys can be removed after 24 hours.
Kafka exactly-once semantics
Introduced in Kafka 0.11 (2017): idempotent producers and transactions. Kafka Streams exposes it as processing.guarantee=exactly_once_v2. It covers reads and writes within Kafka, not side effects in external systems.
Amazon SQS
Standard queues are at-least-once and may deliver out of order. FIFO queues deduplicate messages with the same deduplication id within a 5-minute window; consumers still need idempotency for longer gaps and their own side effects.
Apache Flink
Flink uses checkpoints for exactly-once state inside the job and two-phase commit sinks (for example to Kafka) for exactly-once output to systems that support transactions.
UPI and card payments
Payment networks give each transaction a unique reference, and merchants use their own order id to avoid charging twice. When a response is lost, the merchant checks status instead of retrying the debit blindly; reconciliation files catch anything left uncertain.
In a project
Your notification service consumes OrderShipped events and sends an SMS. During a broker failover and a consumer rebalance, about 3% of customers got the same SMS two or three times, and the SMS bill for the day went up noticeably.
- Accept that Kafka redelivers after rebalances and crashes; the fix is in the consumer, not in hoping for no duplicates.
- Add a dedupe key: (event_id, channel). Before sending, do SET sms:<event_id> 1 NX EX 604800 in Redis, or insert into a Postgres sent_notifications table with a unique key.
- Order the steps carefully: claim the key, send, then mark status sent. If sending fails, release the claim so a retry can happen; if the process dies after sending, the claim stays and stops a duplicate.
- Pass the event id as the client reference to the SMS provider where supported, so the provider can also drop duplicates.
- Keep the claim window longer than topic retention and maximum replay time.
- Add a metric for 'duplicates suppressed' to see how often redelivery really happens.
Impact: Duplicate SMS drop to near zero, the SMS bill falls back to normal, and the team can replay topics after incidents without fear of spamming users. The rare case of a crash between send and claim is accepted and documented as at-most-one-extra.
Trade-offs
Use it when
- At-least-once plus idempotency: payments, wallets, orders, inventory, notifications, almost everything
- At-most-once: metrics samples, telemetry, live location pings where the next update replaces the lost one
- Kafka transactions: stream processing that reads from and writes to Kafka only
Avoid it when
- Promising 'exactly-once' end to end because the broker advertises it
- Using Kafka transactions to protect writes to an external database; they cannot cover it
- At-most-once for anything involving money, orders or user-visible state
- Keeping dedupe ids forever in a hot store; expire them after the redelivery window
Common mistakes
- Saying 'Kafka gives exactly-once, so duplicates cannot happen' for a consumer that sends emails or writes to a database.
- Committing offsets before processing, turning at-least-once into silent message loss.
- Doing the dedupe check and the side effect in separate transactions, so a crash between them causes a loss or a duplicate.
- Using non-idempotent operations like 'balance = balance + 50' with no event id check.
- Generating a new idempotency key on each retry, which defeats the purpose.
- Retrying a payment call after a timeout without checking whether the first attempt succeeded.
Interview questions
Quick revision
- At-most-once: may lose, never duplicates. At-least-once: never loses, may duplicate.
- Exactly-once delivery is impossible in general; exactly-once effect is achievable.
- Effectively-once = at-least-once + idempotent processing.
- Kafka EOS = idempotent producer + transactions, inside Kafka only.
- Process, then commit offset/ack.
- Dedupe row and side effect in one transaction.
- Idempotency keys: generated once by the client, reused on every retry.
- Prefer state transitions and upserts over increments.
- External calls: idempotency key, status check, reconciliation.
Check yourself
1. A consumer charges a card via an external API after reading from Kafka with transactions enabled. What delivery effect does the card charge have without extra work?
2. Which consumer order gives at-least-once delivery?
3. Which operation is naturally idempotent?
4. Why must the dedupe insert be in the same transaction as the business write?
See it in a full design
These walkthroughs use this lesson.
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.