KafkaIQ

5.2 Idempotent Producer

Module Overview

Goal: Understand how idempotent producers prevent duplicates and when to use them.

What You'll Learn:

  • What idempotence means in Kafka
  • How idempotent producers work internally
  • Producer ID (PID) and sequence numbers
  • Configuration and enabling idempotence
  • Limitations and edge cases
  • When idempotence is not enough

Prerequisites:

  • Module 5.1 (Delivery Semantics Explained) — understand at-least-once and duplicates

1. The Duplicate Problem

Scenario: Network Failure After Write

Time  Producer                    Leader Broker
─────┼────────────────────────────┼────────────────
  1  │ send(record-100)            │
  2  │ ───── record-100 ─────→     │ Writes to log (offset 50)
  3  │                             │
  4  │ X←─── ack ────              │ Ack lost in network!
  5  │ TIMEOUT! Retry...           │
  6  │ ───── record-100 ─────→     │ Duplicate write (offset 51)
  7  │ ←───── ack ────             │
  8  │ ✅ Success                  │ ⚠️ Duplicate!

Result: record-100 written twice (offsets 50 and 51).

Problem: At-least-once delivery causes duplicates on retries.


2. What is Idempotence?

Definition

Idempotence: Same operation can be applied multiple times without changing the result beyond the initial application.

In Kafka: Sending the same message multiple times results in exactly one write.

send(message) → Write once
send(message) → No-op (already written)
send(message) → No-op (already written)

Result: Message appears exactly once in the log

Mathematical Analogy

Non-idempotent: x = x + 1
  Call 1: x = 5 + 1 = 6
  Call 2: x = 6 + 1 = 7  ← Different result!

Idempotent: x = 5
  Call 1: x = 5
  Call 2: x = 5  ← Same result
  Call 3: x = 5  ← Same result

3. How Idempotent Producers Work

Core Mechanism

Kafka uses two identifiers to detect duplicates:

  1. Producer ID (PID): Unique ID assigned to producer
  2. Sequence Number: Monotonically increasing per partition
┌──────────────────────────────────────────────┐
│         Producer (PID = 12345)               │
├──────────────────────────────────────────────┤
│                                              │
│  Partition 0: seq=0, 1, 2, 3, ...            │
│  Partition 1: seq=0, 1, 2, 3, ...            │
│  Partition 2: seq=0, 1, 2, 3, ...            │
│                                              │
└──────────────────────────────────────────────┘

Each record sent includes:

  • Producer ID (PID)
  • Sequence number for that partition

Broker-Side Duplicate Detection

┌──────────────────────────────────────────────┐
│            Broker (Leader)                   │
├──────────────────────────────────────────────┤
│  Last known sequence for PID 12345:          │
│    Partition 0: seq=99                       │
│    Partition 1: seq=42                       │
│    Partition 2: seq=15                       │
└──────────────────────────────────────────────┘

Incoming record: PID=12345, Partition=0, Seq=100
  ├─ Expected: 99 + 1 = 100 ✅
  └─ Action: Write to log, update last known seq to 100

Incoming record: PID=12345, Partition=0, Seq=100 (retry)
  ├─ Expected: 100 + 1 = 101, got 100 ⚠️
  └─ Action: Duplicate detected → Ack without writing

Result: Broker acknowledges but doesn't write duplicate.


Detailed Example

Producer (PID=12345) sends to Partition 0:

Time  Producer                    Broker
─────┼────────────────────────────┼────────────────────────────
  1  │ send(msg-1, seq=0)          │
  2  │ ────────────────→            │ seq=0: write (offset 10)
  3  │ ←──── ack ──────             │ last_seq=0
  4  │                             │
  5  │ send(msg-2, seq=1)          │
  6  │ ────────────────→            │ seq=1 (expected 0+1): write (offset 11)
  7  │ X←─── ack ─────              │ ack lost, last_seq=1
  8  │ TIMEOUT! Retry msg-2        │
  9  │ send(msg-2, seq=1) [retry]  │
 10  │ ────────────────→            │ seq=1 (duplicate! expected 1+1=2)
 11  │ ←──── ack ──────             │ Ack without writing
 12  │ ✅ Success                  │ ✅ No duplicate in log

Broker log:

offset 10: msg-1 (seq=0)
offset 11: msg-2 (seq=1)
          ← No duplicate of msg-2

4. Enabling Idempotent Producer

Configuration

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// Enable idempotence
props.put("enable.idempotence", "true");

That's it! One configuration property.


Automatic Configuration Changes

When enable.idempotence=true, Kafka automatically sets:

// These are set automatically (you don't need to set them):
props.put("acks", "all");                            // Was: can be 0, 1, all
props.put("retries", Integer.MAX_VALUE);             // Was: default 0
props.put("max.in.flight.requests.per.connection", 5); // Was: default 5

Why?

  • acks=all: Ensures durability (required for idempotence)
  • retries=MAX_VALUE: Allows retries (needed to recover from failures)
  • max.in.flight.requests=5: Allows pipelining while maintaining order

Default in Modern Kafka

// Kafka 3.0+
// Idempotence is ENABLED BY DEFAULT
props.put("enable.idempotence", "true"); // Default!

You can explicitly disable it (not recommended):

props.put("enable.idempotence", "false"); // Opt out

5. Producer ID (PID) Assignment

How PID is Assigned

1. Producer starts
2. Producer requests PID from broker
3. Broker assigns unique PID (e.g., 12345)
4. Producer uses this PID for all records

PID is session-scoped (lost on restart)

PID Lifecycle

// Create producer
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
// → Broker assigns PID=12345

producer.send(record1); // PID=12345, seq=0
producer.send(record2); // PID=12345, seq=1

producer.close();

// Create new producer
KafkaProducer<String, String> producer2 = new KafkaProducer<>(props);
// → Broker assigns NEW PID=67890 (different from before)

producer2.send(record3); // PID=67890, seq=0 (resets)

Key Point: PID is NOT persistent. New producer → New PID.


6. Sequence Numbers

Per-Partition Sequence

Each producer maintains a separate sequence number per partition:

Producer (PID=12345):

Partition 0: seq=0 → seq=1 → seq=2 → seq=3 ...
Partition 1: seq=0 → seq=1 → seq=2 → seq=3 ...
Partition 2: seq=0 → seq=1 → seq=2 → seq=3 ...

Why per-partition?

  • Records to different partitions are independent
  • Allows parallel sending to multiple partitions

Sequence Number Wrap-Around

Sequence numbers are 32-bit integers (0 to 2^31 - 1).

seq=0, 1, 2, ..., 2147483647 (max int)
seq=0, 1, 2, ... (wraps around)

Broker handles wrap-around correctly (no duplicates).


7. Ordering with Idempotence

Ordering Guarantee

Idempotent producer guarantees:

  • Records sent to same partition are delivered in order
  • Even with retries and multiple in-flight requests
producer.send(new ProducerRecord<>("topic", 0, "key", "msg-1"));
producer.send(new ProducerRecord<>("topic", 0, "key", "msg-2"));
producer.send(new ProducerRecord<>("topic", 0, "key", "msg-3"));

// Partition 0 will have: [msg-1][msg-2][msg-3] (in order)

Even if:

  • msg-1 is retried
  • msg-2 and msg-3 are sent before msg-1 completes
  • Network reorders packets

How Ordering is Preserved

Broker uses sequence numbers to reorder:

Time  Producer sends         Broker receives         Broker writes
─────┼───────────────────────┼───────────────────────┼──────────────
  1  │ msg-1 (seq=0)          │                        │
  2  │ msg-2 (seq=1)          │                        │
  3  │ msg-3 (seq=2)          │                        │
  4  │                        │ msg-2 (seq=1) arrives  │ Wait for seq=0
  5  │                        │ msg-3 (seq=2) arrives  │ Wait for seq=0
  6  │                        │ msg-1 (seq=0) arrives  │ Write seq=0,1,2
  7  │ ←── all acks ────────  │                        │

Result: Broker writes in sequence order, not arrival order.


8. Limitations of Idempotent Producer

Limitation 1: Session-Scoped Only

Problem: PID is lost on producer restart.

// Producer 1
KafkaProducer producer1 = new KafkaProducer<>(props); // PID=12345
producer1.send(record1); // seq=0

// Producer 1 crashes and restarts
KafkaProducer producer2 = new KafkaProducer<>(props); // PID=67890 (new!)
producer2.send(record1); // seq=0 (new sequence)

// Result: If record1 was not acked, it may be duplicated

Limitation: Duplicates can occur across producer restarts.

Solution: Use transactional producer (Module 5.3).


Limitation 2: Single Producer Instance

Problem: Multiple producers can create duplicates.

// Producer A
KafkaProducer producerA = new KafkaProducer<>(props); // PID=12345
producerA.send(record1);

// Producer B (different instance, same application)
KafkaProducer producerB = new KafkaProducer<>(props); // PID=67890
producerB.send(record1);

// Result: record1 written twice (different PIDs)

Limitation: Idempotence is per-producer instance, not application-wide.

Solution: Use transactional producer with same transactional ID.


Limitation 3: Single Partition Only (for duplicate prevention)

Problem: Idempotence prevents duplicates within a partition, not across partitions.

producer.send(new ProducerRecord<>("topic", 0, "key", "data")); // Partition 0
producer.send(new ProducerRecord<>("topic", 1, "key", "data")); // Partition 1

// If both fail and retry, both may duplicate
// No cross-partition duplicate detection

Limitation: Each partition tracked independently.

Solution: Use transactions for atomic multi-partition writes.


9. Error Scenarios with Idempotent Producer

OutOfOrderSequenceException

When it happens:

// Expected sequence: 0, 1, 2, 3, ...
// Broker receives: 0, 1, 3 (missing 2!)

// Broker throws: OutOfOrderSequenceException

Causes:

  • Bug in producer logic
  • Network issues causing lost messages
  • Broker/producer version mismatch

Producer action:

  • Fatal error
  • Producer enters error state
  • Must be closed and recreated

UnknownProducerIdException

When it happens:

// Producer sends with PID=12345, seq=10
// But broker has no state for PID=12345 (expired or never seen)

// Broker throws: UnknownProducerIdException

Causes:

  • Producer inactive too long (broker expired state)
  • Broker restarted

Producer action:

  • Fatal error
  • Producer must be recreated

10. Performance Impact

Overhead

Additional work:

  • Producer tracks sequence numbers (in-memory)
  • Broker tracks last sequence per (PID, partition)
  • Broker performs duplicate check on every write

Performance impact:

  • Minimal (<5% throughput reduction in most cases)
  • No disk I/O overhead (metadata in memory)
  • Slightly higher latency (duplicate check)

Benchmark Example

Configuration: acks=all, 100 byte messages

Without idempotence:
  Throughput: 105,000 msg/sec
  Latency (p99): 10ms

With idempotence:
  Throughput: 102,000 msg/sec (-3%)
  Latency (p99): 11ms (+1ms)

Conclusion: Overhead is negligible for the guarantee provided.


11. Practical Usage

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

// Idempotence (default in Kafka 3.0+)
props.put("enable.idempotence", "true");

// These are set automatically, but shown for clarity:
// props.put("acks", "all");
// props.put("retries", Integer.MAX_VALUE);
// props.put("max.in.flight.requests.per.connection", 5);

// Other recommended settings
props.put("delivery.timeout.ms", 120000);
props.put("compression.type", "lz4");

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

When to Use Idempotent Producer

✅ Use Idempotent Producer When:

  • You need at-least-once delivery without duplicates
  • Single producer instance per topic/partition
  • No cross-partition atomicity needed
  • No need for exactly-once across restarts

Use Cases:

  • Standard production applications
  • Event logging
  • Metrics ingestion
  • User activity tracking

When Idempotence is NOT Enough

❌ Need Transactional Producer When:

  • Writing to multiple topics/partitions atomically
  • Exactly-once across producer restarts
  • Read-process-write patterns (e.g., Kafka Streams)

Next Module: 5.3 — Exactly-Once Semantics (Transactions)


12. Comparing Idempotence Modes

Non-Idempotent vs Idempotent

┌─────────────────────────────────────────────────────────┐
│                  NON-IDEMPOTENT                         │
├─────────────────────────────────────────────────────────┤
│  acks=1                                                 │
│  retries=0                                              │
│                                                         │
│  send(msg) → ack lost → LOST                            │
│  Delivery: At-most-once                                 │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│       NON-IDEMPOTENT WITH RETRIES                       │
├─────────────────────────────────────────────────────────┤
│  acks=all                                               │
│  retries=MAX_VALUE                                      │
│  enable.idempotence=false                               │
│                                                         │
│  send(msg) → ack lost → retry → DUPLICATE               │
│  Delivery: At-least-once (may duplicate)                │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│                  IDEMPOTENT                             │
├─────────────────────────────────────────────────────────┤
│  acks=all                                               │
│  retries=MAX_VALUE                                      │
│  enable.idempotence=true                                │
│                                                         │
│  send(msg) → ack lost → retry → DETECTED, NOT WRITTEN  │
│  Delivery: Exactly-once (per producer session)          │
└─────────────────────────────────────────────────────────┘

Summary

Key Takeaways

✅ Idempotent producer prevents duplicates from retries
✅ Enabled via enable.idempotence=true (default in Kafka 3.0+)
✅ Uses Producer ID (PID) and sequence numbers
✅ Broker detects duplicates and acks without writing
✅ Preserves ordering even with retries
✅ Session-scoped: Lost on producer restart
✅ Per-partition: Duplicates prevented within partition
✅ Minimal performance overhead (<5%)
✅ Automatically sets: acks=all, retries=MAX_VALUE


When to Use

Standard production application?           → ✅ Use idempotence
Need exactly-once across restarts?         → ❌ Use transactions (5.3)
Need atomic multi-partition writes?        → ❌ Use transactions (5.3)
OK with duplicates on producer restart?    → ✅ Use idempotence
Single producer instance?                  → ✅ Use idempotence

Configuration Checklist

  • enable.idempotence=true (default)
  • acks=all (automatic)
  • retries=Integer.MAX_VALUE (automatic)
  • delivery.timeout.ms set appropriately
  • Error handling for OutOfOrderSequenceException
  • Error handling for UnknownProducerIdException

What's Next

  • Module 5.3: Exactly-Once Semantics (Producer Side — Transactions)
    Learn transactional producers for cross-partition exactly-once

  • Module 6.1: Error Handling & Failure Scenarios
    Handle producer errors and failures


Previous Module: ← 5.1 - Delivery Semantics Explained
Next Module: 5.3 - Exactly-Once Semantics (Producer Side) →