Kafka Ordering Guarantees and Message Deduplication
Kafka preserves record order inside a partition, not across an entire multi-partition topic. That distinction affects partition-key design, consumer concurrency, retries, scaling, and every workflow where events must be applied in a predictable sequence.
Ordering also does not prevent duplicates. Producer retries, consumer crashes, external API timeouts, and replay can cause the same logical operation to be attempted more than once. Reliable Kafka systems therefore treat ordering and deduplication as related but separate correctness problems.
Table of Contents
- What Kafka Ordering Actually Guarantees
- Partition Keys Define the Ordering Boundary
- How Ordering Breaks in Production
- Where Duplicate Messages Come From
- Producer Idempotence
- Consumer-Side Deduplication
- Handling Out-of-Order Events
- Practical Payment Processing Example
- Replay Without Duplicating Side Effects
- Choosing the Right Correctness Strategy
- Production Mistakes to Avoid
- What to Monitor
- Conclusion
What Kafka Ordering Actually Guarantees
A Kafka partition is an ordered log. Records appended to one partition receive increasing offsets, and a consumer reading that partition observes records according to those offsets.
Suppose an order-events partition contains:
offset 810: created → 811: paid → 812: shipped → 813: delivered
Kafka preserves this partition order.
Now suppose the topic has eight partitions. Kafka does not provide a meaningful global ordering relationship between offset 810 in partition 2 and offset 400 in partition 7. Each partition has its own independent log.
This means a requirement such as "all events must be processed in exactly the order they happened" is usually too broad. The practical question is:
Which events must be ordered relative to each other?
For an order system, events belonging to one order may require ordering while unrelated orders do not. Kafka can preserve the first requirement while processing thousands of different orders concurrently.
Partitions and offsets are covered in more detail in Kafka Topics, Partitions, and Offsets Explained.
Partition Keys Define the Ordering Boundary
Kafka can preserve ordering only when related records are routed to the same partition. The producer's partition key therefore defines an important part of the application's consistency model.
If order lifecycle events must remain ordered, the producer can use order_id:
import json
from confluent_kafka import Producer
producer = Producer({
"bootstrap.servers": "kafka-1:9092,kafka-2:9092,kafka-3:9092",
"acks": "all",
"enable.idempotence": True,
})
event = {
"event_id": "evt_8129",
"event_type": "order.paid",
"order_id": "ord_92814",
}
producer.produce(
topic="order-events",
key=event["order_id"],
value=json.dumps(event),
)
Events using the same key are routed consistently according to the producer's partitioning behavior, allowing the lifecycle of ord_92814 to remain within one partition.
Choosing the Right Key
The best partition key is usually the smallest business boundary that requires ordering.
Consider a banking system. Using account_id can preserve the sequence of transactions for one account:
deposit → purchase → refund → withdrawal
Transactions belonging to different accounts can still be processed in parallel.
Using customer_id would create a broader ordering boundary. Every account belonging to one customer would share partition placement even if cross-account ordering is unnecessary.
Using country would be worse for most workloads. Millions of unrelated accounts could share a key, producing hot partitions and unnecessary serialization.
Do not serialize more data than the business invariant requires.
Ordering vs Parallelism
Ordering and parallelism naturally compete.
A topic with one partition provides a simple global order, but only one consumer in a consumer group can actively own that partition. This can severely limit throughput.
A topic with 100 well-distributed partitions allows much greater parallelism, but there is no global order across those partitions.
| Design | Ordering | Parallelism |
|---|---|---|
| One partition | Global topic order | Very limited |
| Partition by entity ID | Per-entity order | High when keys are distributed |
| Random distribution | No useful entity ordering | High |
Most scalable business systems therefore preserve ordering only per entity, aggregate, account, shipment, payment, or another meaningful domain boundary.
How Ordering Breaks in Production
Correct partitioning is necessary for ordering, but it is not sufficient. Application processing can reorder effects after Kafka delivers records in the correct sequence.
Consumer Concurrency
Suppose one consumer reads these records in order:
order.created → order.paid → order.cancelled
The application then submits each event independently to a worker pool.
If order.created requires a slow database call while order.paid completes quickly, the database may observe:
order.paid → order.created → order.cancelled
Kafka delivered correctly. The application reordered processing.
If ordering matters within a partition, processing should preserve sequential execution for that partition or use a more sophisticated model that serializes work by the required business key.
This does not mean the whole consumer must be single-threaded. Different partitions can be processed concurrently while records within each partition remain ordered.
Retries and Dead Letter Topics
Retry topics intentionally remove failed events from the original partition flow.
Suppose a shipment partition contains:
picked_up → in_transit → delivered
If in_transit fails and is moved to a five-minute retry topic while the main consumer continues, delivered may be processed first.
The resulting business processing order becomes:
picked_up → delivered → in_transit
That may be harmless for analytics but incorrect for a shipment state machine.
For strict per-key workflows, a failed record may need to block subsequent records for the same key, or the application may need sequence validation that rejects later state transitions until missing state arrives.
Retry and dead-letter trade-offs are covered in Kafka Reliability: Retries, Dead Letter Topics, and Failure Handling.
Where Duplicate Messages Come From
Duplicates are a normal consequence of distributed systems when an operation succeeds but acknowledgement of that success is lost.
Consider a producer:
- The producer sends event X.
- The broker successfully stores X.
- The response is lost because of a network timeout.
- The producer cannot know whether X was stored.
- The producer retries.
Without protection, both attempts can become records.
The consumer side has the same ambiguity:
- A consumer reads event X.
- It writes the business result to PostgreSQL.
- The process crashes before committing the Kafka offset.
- The consumer restarts.
- Kafka delivers X again.
Kafka is behaving correctly. From Kafka's perspective, the consumer never confirmed that processing had advanced beyond X.
Other duplicate sources include manual replay, retry topics, application bugs, duplicated upstream requests, and producers publishing the same logical event under different Kafka requests.
This is why duplicate Kafka records and duplicate business operations are not exactly the same problem.
Producer Idempotence
Kafka producer idempotence protects against certain duplicates caused by producer retries.
A production producer can enable it explicitly:
from confluent_kafka import Producer
producer = Producer({
"bootstrap.servers": "kafka-1:9092,kafka-2:9092,kafka-3:9092",
"acks": "all",
"enable.idempotence": True,
})
Kafka can identify repeated producer requests within the idempotent producer protocol and avoid appending the retry as another record.
This is important, but it does not provide universal business deduplication.
If an application independently calls:
publish_order_created(order_id="ord_92814")
publish_order_created(order_id="ord_92814")
those may be two legitimate producer operations from Kafka's perspective even if the application considers the second one a mistake.
Producer idempotence also does not prevent a consumer from executing the same database update twice after redelivery.
Kafka producer idempotence protects the producer-to-broker write path. Business idempotency protects the application.
Producer delivery behavior is covered in Kafka Producers Explained: Partitioning, Batching, and Delivery Guarantees.
Consumer-Side Deduplication
Consumer-side deduplication prevents repeated delivery from producing repeated business effects.
There are two common approaches: remembering processed event identities and designing the business operation itself to be idempotent.
Deduplicate by Event ID
Every logical event can carry a globally unique stable identifier:
{
"event_id": "evt_payment_82191",
"event_type": "payment.authorized",
"payment_id": "pay_9912",
"order_id": "ord_92814",
"amount": "149.90",
"currency": "USD"
}
The consumer stores the event ID in the same transaction as its business change.
BEGIN;
INSERT INTO processed_events (
consumer_name,
event_id,
processed_at
)
VALUES (
'order-payment-consumer',
'evt_payment_82191',
CURRENT_TIMESTAMP
)
ON CONFLICT (consumer_name, event_id) DO NOTHING;
UPDATE orders
SET payment_status = 'authorized'
WHERE id = 'ord_92814';
COMMIT;
In a real implementation, the update should execute only when insertion into processed_events actually succeeds. If the event ID already exists, the transaction should skip the business mutation.
This approach works well when one event can trigger complex operations that cannot easily be represented by one natural database constraint.
Its cost is additional storage and database work. Deduplication tables also need a retention policy based on how far back duplicates or replays may occur.
Prefer Business Idempotency When Possible
Sometimes the domain already provides a stronger deduplication key.
Suppose an Inventory consumer receives a request to reserve product prd_501 for order ord_92814.
The database can enforce one reservation per order and product:
CREATE UNIQUE INDEX inventory_reservation_order_product
ON inventory_reservations (order_id, product_id);
Processing then becomes naturally idempotent:
INSERT INTO inventory_reservations (
order_id,
product_id,
quantity
)
VALUES (
'ord_92814',
'prd_501',
2
)
ON CONFLICT (order_id, product_id) DO NOTHING;
This protects against more than Kafka redelivery. It can also prevent duplicates caused by API retries, producer bugs, or manual replay.
Business-level constraints are often stronger because they express what must be unique rather than which event IDs have previously been observed.
Handling Out-of-Order Events
Some architectures cannot guarantee that every relevant event arrives in order. Events may originate from different topics, different systems, retry paths, or asynchronous integrations.
In these cases, the consumer should not assume arrival order represents valid business order.
A useful approach is to include an entity version or sequence number:
{
"event_id": "evt_order_821",
"event_type": "order.updated",
"order_id": "ord_92814",
"version": 14,
"status": "shipped"
}
The consumer can update its projection only when the incoming version is newer:
UPDATE order_projection
SET
status = 'shipped',
version = 14
WHERE
order_id = 'ord_92814'
AND version < 14;
If version 13 arrives after version 14, it does not overwrite the newer state.
This pattern is especially useful for materialized views, caches, search indexes, and integrations where the desired result is the latest entity state rather than execution of every transition.
It is not suitable for every domain. A financial ledger cannot simply discard an older transaction because a newer one arrived first. Every transaction may need to be applied in sequence.
The correct strategy therefore depends on whether events represent state snapshots, state transitions, or independent facts.
Practical Payment Processing Example
Consider a Payment Service that consumes payment.requested events and calls an external payment provider.
A payment event contains:
{
"event_id": "evt_99172",
"event_type": "payment.requested",
"payment_id": "pay_72191",
"order_id": "ord_92814",
"amount": "149.90",
"currency": "USD"
}
The topic is partitioned by payment_id, keeping events for the same payment together while allowing unrelated payments to execute concurrently.
The consumer then calls the provider using the stable payment identity:
result = payment_provider.charge(
amount_cents=14990,
currency="USD",
idempotency_key="pay_72191",
)
Now consider the difficult failure:
- Payment Service sends the charge request.
- The provider successfully charges the card.
- The response is lost.
- The Payment Service sees a timeout.
- The Kafka event is retried.
Without idempotency, the second attempt can charge the card again.
With pay_72191 as the provider's idempotency key, repeated attempts refer to the same logical payment operation.
After a successful provider response, Payment records the result locally and creates its outgoing event using a transactional outbox:
BEGIN;
UPDATE payments
SET
status = 'authorized',
provider_transaction_id = 'txn_88271'
WHERE
id = 'pay_72191'
AND status = 'pending';
INSERT INTO outbox_events (
event_id,
aggregate_id,
event_type,
payload
)
VALUES (
'evt_payment_authorized_72191',
'pay_72191',
'payment.authorized',
'{"payment_id":"pay_72191","order_id":"ord_92814"}'
)
ON CONFLICT (event_id) DO NOTHING;
COMMIT;
The design uses several independent protections:
- Kafka partitioning preserves ordering for one payment.
- Producer idempotence protects against certain producer retry duplicates.
- Provider idempotency key prevents repeated external charges.
- Database state transition prevents an already authorized payment from being authorized again.
- Transactional outbox keeps local payment state and outgoing event publication recoverable.
No single Kafka setting provides all of these guarantees. Reliability comes from protecting every side-effect boundary.
Replay Without Duplicating Side Effects
Kafka's retained log makes replay useful for rebuilding projections, recovering from consumer bugs, and introducing new consumers.
Replay can also be dangerous when consumers perform external side effects.
Suppose a notification consumer resets its offset and rereads six months of order.shipped events. If processing blindly sends an email for every record, customers may receive months-old shipping notifications again.
A search-index consumer is different. Reprocessing an event that sets document ord_92814 to its latest representation may be naturally idempotent.
Before making a stream replayable, classify its side effects:
- State replacement. Usually easier to replay safely.
- Idempotent upsert. Usually safe with stable keys.
- Increment or append. Requires deduplication or reconstruction strategy.
- External irreversible action. Requires explicit idempotency protection.
- Human notification. Often needs replay suppression or separate projection logic.
A strong architecture separates rebuilding internal state from triggering irreversible external effects whenever practical.
Choosing the Right Correctness Strategy
Different Kafka workloads require different combinations of ordering and deduplication.
| Workload | Ordering | Duplicate Protection |
|---|---|---|
| Payment processing | Per payment or account | Business ID + provider idempotency |
| Inventory reservation | Per product or reservation boundary | Database uniqueness |
| Search indexing | Latest version matters | Entity version + idempotent upsert |
| Email delivery | Usually weak | Notification ID |
| Analytics ingestion | Often weak | Event ID when exact counts matter |
Strict ordering should not be added simply because Kafka supports it. It reduces available concurrency and complicates failure recovery.
Likewise, universal deduplication tables may be unnecessary when a database uniqueness constraint already expresses the correct business invariant.
The best strategy is the smallest mechanism that protects the actual business correctness requirement.
Production Mistakes to Avoid
Ordering and duplicate bugs often appear only during retries, outages, rebalances, or replay, which makes them easy to miss during normal testing.
- Assuming Kafka provides topic-wide ordering. Ordering exists within partitions. Put related events in the same partition when their relative order matters.
- Using a low-cardinality partition key. Keys such as country or event type can create hot partitions. Use the narrowest well-distributed business key that needs ordering.
- Processing one partition concurrently without preserving sequence. Worker pools can reorder database effects even when Kafka delivery is ordered.
- Moving failed records to retry topics without checking ordering requirements. Later records can overtake the failed event.
- Assuming producer idempotence solves consumer duplicates. Consumer crashes and replay still require idempotent business processing.
- Deduplicating outside the business transaction. A crash between deduplication and the business write can leave inconsistent state. Protect both atomically when possible.
- Generating a new event ID on every retry. The stable identity of the logical event should survive retries and replay.
- Replaying side-effecting consumers blindly. Historical events can resend notifications, duplicate external operations, or increment counters again.
What to Monitor
Ordering violations are primarily correctness problems, so infrastructure metrics alone will not reveal them. Monitoring should include business-level signals.
- Duplicate event rate. Count events rejected by event-ID or business-key deduplication.
- Out-of-order event rate. Track sequence or version regressions where applicable.
- Consumer lag per partition. One blocked ordered partition can be hidden by healthy partitions.
- Retry volume by partition and key. Repeated failures can identify entities preventing ordered progress.
- Dead letter volume. Detect records removed from normal processing.
- Partition traffic distribution. Records and bytes per partition reveal key skew.
- Idempotency conflicts. Track database uniqueness conflicts and external provider duplicate detections.
- Invalid state transitions. Events such as
delivered → in_transitcan expose reordering or producer bugs. - Replay volume and age. Distinguish normal processing from historical recovery traffic.
For critical state machines, an invalid transition counter can be more useful than generic consumer error rate. Kafka may be perfectly healthy while events are being applied in a business-invalid sequence.
Conclusion
Kafka guarantees ordering within a partition, so partition keys should represent the smallest business boundary that actually requires ordered processing. Preserving unnecessary global ordering sacrifices parallelism and limits scalability.
Ordering can still be broken after delivery by concurrent processing, retry topics, and independent event sources. Consumers should therefore preserve sequence where required or use explicit versions and state-transition validation when out-of-order arrival is possible.
Duplicates are a separate problem. Producer idempotence protects part of the Kafka write path, while consumer idempotency, database constraints, stable event IDs, and external idempotency keys protect business side effects.
The central production principle is: use Kafka ordering to protect the smallest required business sequence, and make every important side effect safe to execute more than once.
Comments (0)