Producer Idempotence Internals: PID & Sequence Numbers in Kafka

Producer Idempotence Internals: PID & Sequence Numbers in Kafka

How a Kafka broker recognizes a message it has already stored, using a producer identity, an epoch, and a running sequence number, so that retries after network trouble never leave duplicate records in your topics.

Imagine you post an important parcel and the post office gives you a tracking slip. The next day you cannot tell whether the parcel arrived, so you post an identical parcel with the same slip number written on it. A careful sorting office looks at the slip, sees it already handled that number, and quietly throws the second parcel away. The recipient receives exactly one parcel, even though you sent two. You never had to know whether the first one made it.

Kafka’s idempotent producer works in precisely this spirit. Every producer gets an identity, every message batch it sends carries a running number, and every broker remembers the most recent numbers it has seen. When the same batch shows up twice, the broker recognizes it and refuses to store it again. This tutorial opens that mechanism up piece by piece, from the vocabulary to the byte-level header fields, the failure cases, and the operational habits that keep it healthy in production.

1The Duplicate Problem

To appreciate the solution, first watch the problem happen in slow motion.

Why a producer retries at all

A Kafka producer sends a batch of messages to the broker that leads a partition. The broker writes the batch to its log, waits for the replicas it needs, and then sends back an acknowledgement. Networks are unreliable, and brokers restart, so any step of that conversation can fail. A producer that gives up at the first failure would lose data, so by design it retries: it sends the very same batch again.

How a retry becomes a duplicate

Retries are safe when the first attempt truly failed. They are dangerous when the first attempt actually succeeded but the acknowledgement got lost on the way back. From the producer’s point of view, a lost request and a lost reply look identical: it simply hears nothing. If it retries in the second case, the broker stores the same messages twice.

Simple Analogy

You shout an order to a busy kitchen and hear no reply. Either the cook did not hear you, or the cook heard you and their answer got lost in the noise. If you shout again, in the first case you get your meal, and in the second case you get two meals. Without a way to tell the cases apart, retrying carries a risk.

sequenceDiagram
    participant P as Producer
    participant B as Broker (partition leader)
    P->>B: Send batch with 3 messages
    B->>B: Write batch to log (offsets 40 to 42)
    B--xP: Acknowledgement lost on the network
    Note over P: No reply before timeout, assume failure
    P->>B: Retry the same 3 messages
    B->>B: Write again (offsets 43 to 45)
    B-->>P: Acknowledged
    Note over B: The log now holds every message twice
        
FIG 1 — A lost acknowledgement turns a safe retry into duplicate records

Why duplicates hurt

Impact 1

Money counted twice

A payment event stored twice can lead to a customer being charged twice or a ledger showing an inflated balance.

Impact 2

Wrong analytics

Counters, sums, and dashboards built from the topic are inflated in ways that are hard to notice and harder to correct.

Impact 3

Repeated side effects

Downstream services may send an email twice, ship an order twice, or trigger an alert twice for one real event.

Impact 4

Broken ordering assumptions

A retry can land after later messages, so a sequence such as “created, updated, deleted” might appear out of order in the log.

!
Common Misconception

Many people believe duplicates only happen when something is badly broken. In truth, one brief network stall or a leader election during a send is enough. At scale, with millions of batches a day, such moments are routine rather than rare.

The two old options

Before idempotence, a team had to pick one of two unpleasant trade-offs. Turn retries off, and any failure could lose data. Keep retries on, and some duplicates would appear. Pushing the burden of deduplication onto every consumer was possible, but it meant every downstream team had to build and maintain its own logic. The idempotent producer moves that responsibility to the place where it can be solved cheaply and once: the broker.

2Delivery Semantics in Plain Words

Three phrases appear in every messaging discussion. Here is what each one really promises.

GuaranteeWhat happens on failureRiskReal-life picture
At most onceDo not retry, accept the lossMessages can disappearDropping a letter in a mailbox and never checking
At least onceRetry until confirmedMessages can be duplicatedSending the parcel again whenever you are unsure
Exactly onceRetry safely, remove duplicatesNeeds extra machinery and has boundariesSending again, with a tracking slip that lets the office discard repeats
Simple Analogy

A teacher collects homework by asking students to put it in a tray. If nobody checks whether homework arrived, some may be lost. If students resubmit whenever unsure, the tray gets copies. If each sheet carries the student’s name and a number, the teacher can quickly notice and discard copies, so the tray holds each sheet once.

Where idempotence sits

The idempotent producer gives you exactly once within a single producer session, for a single partition. That is a smaller promise than “exactly once everywhere”, but it is the essential building block on which larger guarantees, like transactions, are built. Understanding its exact edges is one of the goals of this tutorial, and later chapters return to them in detail.

i
Key Insight

The word idempotent comes from mathematics and simply means “doing it twice has the same effect as doing it once”. Pressing an elevator’s call button ten times summons the elevator once. That is the behavior we want when a producer sends the same batch twice.

Per session
The boundary of the guarantee in time
Per partition
The boundary of the guarantee in space
Broker side
Where duplicates are actually detected

3Core Vocabulary You Need

Each term below is defined once here and then used consistently in the rest of the tutorial.

Term 1

Producer ID (PID)

A large number assigned by the cluster to identify one producer instance. It is the “name on the tracking slip”.

Term 2

Producer epoch

A small counter attached to the PID that grows when the same identity is renewed, letting brokers reject older holders of that identity.

Term 3

Sequence number

A running counter per producer and per partition that numbers each record, starting at zero.

Term 4

Base sequence

The sequence number of the first record inside a batch. The rest of the batch follows consecutively.

Term 5

Record batch

The unit of sending and storing in Kafka: a group of records wrapped with a header that carries the PID, epoch, and base sequence.

Term 6

Producer state

The memory a broker keeps for each producer and partition: the latest sequences, offsets, and epoch it has seen.

Term 7

In-flight request

A batch that has been sent to a broker but not yet acknowledged. The number of these allowed at once is capped.

Term 8

Fencing

Rejecting messages from an old holder of an identity because a newer holder, with a higher epoch, has taken over.

Term 9

Transactional ID

A name chosen by the application, used by transactional producers so the same logical producer keeps its identity across restarts.

Term 10

Duplicate detection window

The number of most recent batches per producer and partition that a broker can compare an incoming batch against.

i
Naming Note

Do not confuse a producer ID with a client ID. A client ID is a friendly label you choose for logs and quotas, and it does not affect deduplication. The PID is assigned by the cluster and is invisible to most applications.

4The Big Picture: Three Simple Ideas

The whole mechanism rests on identity, numbering, and memory.

Idea one: identity

Before sending anything, the producer asks the cluster for a producer identity. From then on, every batch it sends says “I am producer number 4711, in epoch 0”. This tells the broker whose numbering to look at.

Idea two: numbering

For each partition it writes to, the producer keeps a counter. The first record sent to partition 3 is number 0, the second is number 1, and so on. Every batch states the number of its first record and how many records it holds, so the numbers of a batch are fully known.

Idea three: broker memory

For every producer and partition, the broker remembers the number of the last record it stored, and the details of a few recent batches. When a new batch arrives, the broker compares its numbers with what it remembers, and decides whether it is the expected next batch, a repeat of one already stored, or something unexpected.

Simple Analogy

A cinema usher checks tickets. Each ticket has a seat number and a show id. The usher keeps a list of seats already occupied. If the same ticket arrives twice, the usher knows the seat is taken and turns the second copy away politely, without needing to remember the face of the guest.

flowchart TD
    A["Producer starts"] --> B["Ask cluster for a producer identity"]
    B --> C["Receive PID and epoch"]
    C --> D["Number records per partition from zero"]
    D --> E["Send batch stamped with PID, epoch, base sequence"]
    E --> F{"Broker compares with its memory"}
    F -->|"Next expected"| G["Store and remember the new numbers"]
    F -->|"Already seen"| H["Do not store, reply success with original offset"]
    F -->|"Gap or too old"| I["Reject with an error"]
        
FIG 2 — The three ideas working together on every send
“The broker does not guess whether a message is a duplicate. It reads the number on the batch and checks its notebook.”

What the user has to do

In modern Kafka versions, almost nothing. The setting that turns on idempotence is enabled by default in current client releases, as long as the other related settings are compatible. Yet knowing what happens under the hood is essential, because the guarantee has boundaries, and some common configurations quietly switch the feature off or weaken it.

5Architecture and Components

Several parts of Kafka cooperate to make idempotence work. Knowing who does what makes debugging much easier.

On the producer side

1

Record accumulator

Gathers records into batches per partition. The batch is the unit that receives numbers, so this is where sequences are attached.

2

Sender thread

A background thread that drains ready batches and ships them to the right brokers, tracks in-flight requests, and handles retries.

3

Transaction manager

Despite its name, this component also serves plain idempotent producers. It holds the PID and epoch, hands out sequence numbers, and drives recovery after errors.

On the broker side

Component

Any broker (for identity requests)

Answers the initial request for a producer identity for plain idempotent producers, and hands out an unused ID.

Component

Transaction coordinator

Takes over identity management when an application sets a transactional ID, mapping that name to a stable PID and managing its epoch.

Component

Partition leader log

The place where batches are validated, appended, and acknowledged. It owns the authoritative view of the sequence numbers for that partition.

Component

Producer state manager

The per-partition structure that holds the remembered numbers for every producer, and writes its own snapshots to disk.

Component

Follower replicas

Copy batches with their headers intact, and rebuild the same producer state, so a new leader inherits the memory.

Component

Cluster controller

Hands out large blocks of unused producer IDs to brokers, so that two brokers can never give out the same PID.

flowchart LR
    subgraph Client["Producer process"]
        ACC["Record accumulator"]
        SND["Sender thread"]
        TM["Transaction manager: PID, epoch, sequences"]
    end
    subgraph Cluster["Kafka cluster"]
        CTRL["Controller: allocates PID blocks"]
        BR["Broker: identity requests"]
        LEAD["Partition leader: validate, append"]
        FOL["Follower replicas: rebuild producer state"]
    end
    TM --> SND
    ACC --> SND
    SND -->|"identity request"| BR
    BR -->|"needs new block"| CTRL
    SND -->|"batches with PID, epoch, sequence"| LEAD
    LEAD -->|"replicate batches"| FOL
        
FIG 3 — The parts of Kafka involved in idempotent sending
!
Common Mistake

Assuming the deduplication happens in the client library alone. The client only labels each batch with numbers. All duplicate detection happens on the broker that leads the partition, which is why the guarantee works even if the client restarts its network connection.

6The Producer ID: How Identity Is Assigned

The PID is the anchor of the entire design, so it deserves a careful look.

What a PID is

A PID is a 64-bit whole number. That is huge, over nine quintillion possible values, so the cluster will never run out in practice. It is chosen by the cluster, not by the application, which means no two live producers can accidentally pick the same identity.

The initialization handshake

When an idempotent producer starts sending, its first step is to send an initialize producer ID request to a broker. For a plain idempotent producer, any broker can answer. The broker responds with a fresh PID and an epoch, which begins at zero. The producer stores both in memory and stamps every later batch with them.

sequenceDiagram
    participant P as Producer
    participant B as Any broker
    participant C as Cluster controller
    P->>B: Initialize producer ID
    alt Broker has unused IDs in its block
        B->>B: Take next ID from local block
    else Block exhausted
        B->>C: Request a new block of IDs
        C-->>B: Block of unused IDs
        B->>B: Take next ID from new block
    end
    B-->>P: PID and epoch zero
    Note over P: Every future batch carries this PID and epoch
        
FIG 4 — How a producer receives its identity

Why IDs come in blocks

If every identity request had to reach the central controller, the controller would become a bottleneck, and a controller outage would stop new producers from starting. Instead, each broker receives a block of consecutive IDs at a time. It can then hand them out locally, without any coordination, and only goes back to the controller when its block runs out. The controller records which blocks it has given away, so no ID is ever handed out twice, even after a crash or failover.

Simple Analogy

A hospital gives each ward a roll of numbered patient wristbands. The ward hands them out one by one without calling head office each time. When the roll runs out, the ward collects a new roll, and head office notes which rolls have gone to which ward.

A PID is not forever

A plain idempotent producer holds its PID only in memory. If the producer process restarts, it asks for a brand new PID. The broker has no way to link the new identity to the old one. This detail is the source of the most important limitation, explained later: idempotence protects a producer session, not the logical application across restarts.

Producer kindWho answers the identity requestIdentity survives restart?
Plain idempotentAny brokerNo, a new PID every time
TransactionalThe transaction coordinator for the transactional IDYes, same PID with a higher epoch
i
Why it matters

Each new PID means new state on every partition the producer writes to. Applications that create a fresh producer for every request therefore create an ocean of short-lived identities. Later chapters show why this hurts.

7The Producer Epoch: Fencing Old Identities

A PID says who you are. The epoch says which version of you is currently valid.

What the epoch is for

The epoch is a small counter, a 16-bit number, stored right beside the PID. It exists so the cluster can retire an older holder of an identity without inventing a new PID. Whenever a newer holder appears, it receives a higher epoch, and every broker will refuse any batch stamped with a lower epoch for that PID.

Simple Analogy

A hotel reissues a room key card whenever a guest loses one. The new card has a higher version number, and the door lock accepts only the latest version. The old card, even if found under a sofa, no longer opens the door.

Where the epoch changes

Transactional producers restarting

When an application starts a producer with the same transactional ID as before, the coordinator returns the same PID with a bumped epoch. Any older instance still running, a so-called zombie, is now fenced, because its epoch is lower.

Recovering from an unknown state

Newer Kafka clients can bump the epoch on their own after certain recoverable errors, such as when a broker forgot the producer’s state. The producer asks for a fresh epoch and restarts its sequence numbers from zero. This allows work to continue without a full restart.

Plain idempotent producers

Because a plain producer receives a brand new PID on each start, its epoch usually stays at its starting value, and fencing across restarts is not needed, since no other instance shares its PID.

flowchart TD
    A["Instance A holds PID 88, epoch 3"] --> B["Instance A freezes during a long pause"]
    B --> C["Instance B starts with same transactional ID"]
    C --> D["Coordinator returns PID 88, epoch 4"]
    D --> E["Instance A wakes up and sends epoch 3"]
    E --> F{"Broker compares epochs"}
    F -->|"3 is lower than 4"| G["Reject with invalid epoch error"]
    G --> H["Instance A is fenced and must stop"]
        
FIG 5 — Epoch comparison stops a zombie producer
i
Same pattern everywhere

Kafka uses growing numbers to retire the past in several places: leader epochs for partitions, broker epochs for brokers, and controller epochs for the metadata quorum. The producer epoch is the same idea applied to producers.

8Sequence Numbers: The Heart of Deduplication

The sequence number is the number written on the tracking slip. Everything else exists to support it.

One counter per partition

A producer that writes to five partitions keeps five independent counters, one for each partition. Each starts at zero and grows by one for every record sent to that partition. Numbering is per PID and per partition, because the broker that leads one partition knows nothing about the others.

Simple Analogy

A courier company numbers parcels separately for each delivery route. Parcel 7 for route A and parcel 7 for route B have nothing to do with each other, and the depot for each route only tracks its own list.

Numbering records, stamping batches

Although the counter counts records, the message travels in batches. Each batch carries the sequence number of its first record, called the base sequence, together with the count of records. The broker can therefore compute the last sequence number in the batch, and it knows exactly which range of numbers the batch claims.

BatchRecordsBase sequenceLast sequence
First batch to partition 2403
Second batch649
Third batch21011
Fourth batch51216

The golden rule the broker checks

The broker remembers the last sequence number it stored for this producer and partition. The next batch must begin exactly one higher. If the base sequence is exactly one more than the last stored, the batch is the expected next one. This simple rule catches every kind of problem.

Case A

Base equals last plus one

The batch is the next in line. The broker stores it and updates its memory.

Case B

Batch matches a remembered one

It is a retry of something already stored. The broker does not store it again, and answers success with the offsets of the first copy.

Case C

Base is higher than expected

A batch went missing. Accepting this one would leave a hole, so the broker rejects it with an out-of-order error.

Case D

Base is lower and not remembered

It looks like a very old batch that the broker no longer has details about. It cannot safely confirm it, so it rejects.

Sequences wrap around

The sequence number is a 32-bit whole number, roughly two billion values before it wraps. The counter is designed so that, when it reaches its maximum, it wraps back to zero, and the broker’s comparison handles this wrap correctly. A producer that writes a very large number of records to one partition in one session simply cycles through its numbers.

“The sequence number is a promise that says: nothing was skipped, nothing was repeated.”

9Inside the Record Batch Header

Everything idempotence needs is carried in a few fixed fields at the front of each batch.

Kafka stores and transmits records in batches, and each batch begins with a header. In the modern format, three of the header fields are dedicated to producer identity. Because the header is written to disk exactly as sent, the identity information becomes part of the permanent log, and this is precisely what allows a broker to rebuild its memory after a restart.

Header fieldSizePurpose
Base offset8 bytesLog position of the first record in the batch, filled in by the leader
Batch length4 bytesTotal size, so readers can skip to the next batch
Partition leader epoch4 bytesWhich leader wrote this batch, used for truncation logic
Magic1 byteFormat version, marks this as the modern format
Checksum4 bytesDetects corruption
Attributes2 bytesCompression, timestamp type, transactional and control flags
Last offset delta4 bytesHow many records the batch spans
First and maximum timestamp8 bytes eachTime range of the records
Producer ID8 bytesIdentity of the producer that wrote the batch
Producer epoch2 bytesVersion of that identity
Base sequence4 bytesSequence number of the first record
Record count4 bytesHow many records follow
Simple Analogy

A parcel’s shipping label lists the sender number, the label version, and the parcel’s position in the sender’s series. The label is stuck on the box and stays there for the parcel’s entire journey, so any sorting office along the way can read it without opening the box.

Why the header is written to disk

Because the producer fields are stored with every batch, the log itself is a complete record of who wrote what and in what order. If a broker loses its in-memory state, it can walk through the log and reconstruct the memory it needs. Followers receive the exact same bytes, header included, so they can build the same memory as well.

i
Tool tip

Kafka’s log dump tool shows these header fields for each batch in a partition’s log file. It is a very direct way to see the PID, epoch, and sequence numbers of real data, and it is invaluable when investigating suspected duplicates or ordering problems.

flowchart LR
    A["Base offset"] --> B["Length"]
    B --> C["Leader epoch"]
    C --> D["Magic and checksum"]
    D --> E["Attributes"]
    E --> F["Offset delta and timestamps"]
    F --> G["Producer ID"]
    G --> H["Producer epoch"]
    H --> I["Base sequence"]
    I --> J["Record count"]
    J --> K["Records"]
        
FIG 6 — Order of fields at the front of a record batch, with producer fields near the end of the header

10The Send Path, Step by Step

Follow one message from the moment the application hands it to the producer until the acknowledgement returns.

1

Application calls send

The record is serialized, and a partition is chosen from the key or by the partitioner.

2

Record joins a batch

The accumulator adds it to the open batch for that partition, waiting until the batch is full or a short delay passes.

3

Batch is ready and numbered

When the sender drains the batch, the producer assigns the base sequence from its per-partition counter and stamps the PID and epoch. Once assigned, these numbers stay with the batch for all its retries.

4

Sent to the partition leader

The batch travels in a produce request, together with other ready batches for the same broker.

5

Broker validates

The leader compares the PID, epoch, and sequence against its memory, and decides to store, treat as duplicate, or reject.

6

Replicate and acknowledge

Followers copy the batch. Once the required replicas hold it, the leader acknowledges. The producer then completes the callback and advances its state.

sequenceDiagram
    participant App as Application
    participant Acc as Accumulator
    participant Snd as Sender
    participant L as Partition leader
    participant F as Follower
    App->>Acc: send record
    Acc->>Acc: add to batch for partition
    Snd->>Acc: drain ready batch
    Snd->>Snd: stamp PID, epoch, base sequence
    Snd->>L: produce request
    L->>L: validate sequence against memory
    L->>L: append batch and update memory
    F->>L: fetch new batch
    L-->>Snd: acknowledgement with offset
    Snd-->>App: callback success
        
FIG 7 — Journey of a record through an idempotent producer
!
Important detail

Numbers are attached when the batch is first sent, and they never change on retry. If numbers were reassigned on retry, the broker would see a “new” batch and store it again, defeating the whole idea. A retried batch is byte-for-byte the same batch with the same numbers.

11Broker-Side Validation: The Decision Tree

Every batch arriving at a partition leader goes through a small set of questions, always in the same order.

The remembered state

For each producer that has written to a partition, the leader keeps a small entry. It holds the producer’s latest epoch, the last sequence number stored, and compact details of up to five of the most recent batches: their first and last sequence numbers and the offsets where they landed. These details let the broker answer a retry with the correct original offset, rather than a made-up one.

flowchart TD
    A["Batch arrives with PID, epoch, base sequence"] --> B{"Does broker know this PID for this partition?"}
    B -->|"No, and base sequence is zero"| C["Accept as first batch, create entry"]
    B -->|"No, and base sequence not zero"| D["Reject: unknown producer or out of order"]
    B -->|"Yes"| E{"Compare epoch"}
    E -->|"Lower than stored"| F["Reject: producer fenced"]
    E -->|"Higher than stored"| G["Reset entry, accept only if base sequence is zero"]
    E -->|"Equal"| H{"Compare base sequence with expected"}
    H -->|"Equals last plus one"| I["Accept, append, update entry"]
    H -->|"Matches one of last five batches"| J["Duplicate: skip append, return original offset"]
    H -->|"Otherwise"| K["Reject: out of order sequence"]
        
FIG 8 — The validation decision tree run by the partition leader

Outcomes at a glance

SituationBroker actionWhat the producer sees
Expected next batchAppend and rememberNormal success
Exact repeat of a recent batchSkip appendSuccess, with the original offset
Sequence gapRejectOut-of-order sequence error
Lower epoch than knownRejectInvalid epoch or fenced error
PID not known and sequence not zeroRejectUnknown producer error
Higher epoch than knownReset producer state, then evaluateSuccess if the sequence starts at zero
Simple Analogy

A bouncer at a club holds a clipboard. A guest with the next ticket number walks in. A guest with a ticket number already crossed off is told “you are already inside”, and is politely sent to their table rather than admitted again. A guest whose number jumps far ahead is stopped and asked what happened to the numbers in between.

Why rejecting gaps matters

A gap means some earlier batch never reached the broker. If the broker accepted the later batch anyway, the log would silently miss messages and the order of events would be wrong. By rejecting, the broker forces the producer to resend from the point of the gap, which preserves both completeness and order.

12Retries Under Two Kinds of Failure

Return to the problem from chapter one and watch the same failures play out with idempotence turned on.

Failure kind one: the request is lost

The batch never reaches the broker. After a timeout, the producer retries with the same numbers. The broker sees a batch with exactly the next expected sequence, and stores it. There is no duplicate, because the first attempt never happened.

Failure kind two: the reply is lost

The batch reaches the broker and is stored, but the acknowledgement vanishes. The producer retries with the same numbers. This time the broker finds that the batch matches one it remembers, and skips the append. It replies success, reporting the original offset. The producer believes its retry worked, which in a sense it did, and the log contains the messages exactly once.

sequenceDiagram
    participant P as Producer
    participant B as Broker
    P->>B: Batch PID 7, epoch 0, base sequence 20, 3 records
    B->>B: Accept, store at offsets 100 to 102, remember sequences 20 to 22
    B--xP: Acknowledgement lost
    Note over P: Timeout, retry with identical numbers
    P->>B: Same batch PID 7, epoch 0, base sequence 20, 3 records
    B->>B: Matches remembered batch, do not append
    B-->>P: Success with offsets 100 to 102
    Note over B: Log holds the three records once
        
FIG 9 — The same lost-acknowledgement scenario, now safe

Failure kind three: reordering during retries

Suppose two batches are in flight, batch A with sequences 20 to 22 and batch B with sequences 23 to 25. Batch A fails and is retried, but batch B arrives first. Without idempotence, batch B would be stored before batch A, reversing the order. With idempotence, the broker sees that B’s base sequence, 23, does not equal the expected 20. It rejects B with an out-of-order error. The producer resends A and then B, and the log ends up in the correct order.

Result

Lost request

Retry stored once. Order kept.

Result

Lost reply

Retry recognized as a repeat and skipped. Original offsets returned.

Result

Overtaking batches

Later batch rejected until the earlier one lands. Order kept.

Result

Broker restart mid-flight

Producer state is rebuilt from the log, so repeated batches are still recognized.

i
A bonus benefit

Idempotence gives you not just removal of duplicates but also preserved order within a partition when retries are involved. Before it existed, teams that needed strict order had to allow only one request in flight at a time, at a real cost in throughput.

13In-Flight Requests and the Five-Batch Window

Why does Kafka insist on a small number, five, when it comes to outstanding requests?

What “in flight” means

To use the network efficiently, a producer does not wait for one request to be acknowledged before sending the next. It keeps several requests to the same broker outstanding at once. A setting controls how many such unacknowledged requests are allowed per connection.

Simple Analogy

A cashier at a busy till can pass several receipts to a bagging assistant before the first bag is handed over. More receipts in progress means a faster queue, but the assistant needs a system for tracking which bag belongs to which receipt.

The link with the broker’s memory

The broker remembers the details of only the last five batches per producer and partition. That number is closely tied to the limit on in-flight requests. If a producer never has more than five batches outstanding, then any batch that might be retried is guaranteed to be among the five the broker still remembers. The broker can therefore always tell a retry from a genuinely new batch.

flowchart LR
    subgraph Window["Broker remembers last five batches"]
        B1["Batch 1"]
        B2["Batch 2"]
        B3["Batch 3"]
        B4["Batch 4"]
        B5["Batch 5"]
    end
    N["New batch arrives"] --> Q{"Matches one in the window?"}
    Q -->|"Yes"| D["Duplicate: acknowledge without storing"]
    Q -->|"No, and expected next"| S["Store and slide the window forward"]
    Q -->|"No, and not expected"| R["Reject"]
        
FIG 10 — The five-batch window used to recognize repeats

What happens if you go beyond the limit

With more than five batches in flight, a retry of the oldest one could arrive after the broker has already pushed it out of memory. The broker could then no longer recognize it as a repeat. Modern clients therefore refuse to enable idempotence when the setting for in-flight requests is above five, and older clients treat it as a configuration error.

Max in-flight per connectionThroughputOrder with retries and idempotenceAllowed with idempotence?
1LowestPreservedYes
2 to 5Good to highPreserved thanks to sequence checksYes
Above 5Higher, but not useful in most setupsCannot be guaranteedNo
i
Rule of thumb

The default of five in-flight requests is a sweet spot: it lets the network stay busy and it fits perfectly inside the broker’s memory of five batches. Lowering it rarely buys anything beyond lower throughput, and raising it is not permitted.

14Producer State on the Broker: Memory, Snapshots, Expiry

The broker’s notebook must survive restarts and must not grow forever. Both needs shape the design.

Where the state lives

Each partition replica has a producer state manager. It holds a map from PID to that producer’s entry. The map lives in memory for speed, but the truth is always recoverable from the log, because every batch header contains the PID, epoch, and sequence numbers.

Snapshots to speed up recovery

If a broker had to scan the entire log after every restart to rebuild its notebook, startup would be painfully slow for partitions holding many gigabytes. To avoid this, the broker periodically writes a producer state snapshot file next to the partition’s log segments, usually when a log segment rolls over. On restart, the broker loads the latest snapshot and replays only the batches written after it.

1

Batches are appended

Each accepted batch updates the in-memory entry for its producer.

2

A segment rolls

When the active log segment is closed and a new one begins, the broker writes a snapshot of the whole producer map.

3

Restart happens

The broker loads the most recent snapshot as its starting point.

4

Replay the tail

Batches written after the snapshot are read from the log, and the entries are brought fully up to date.

Simple Analogy

A shop counts its stock at the end of each week and files the count. After a power cut, the manager takes the latest count and adds up only the sales since then, instead of recounting every item in the warehouse from scratch.

Expiry keeps the notebook small

A busy cluster sees a large number of producers come and go. If the broker remembered every producer forever, the memory would grow without limit. Entries are therefore removed after a period of inactivity, controlled by an expiration setting whose default is measured in a day. Entries can also disappear when the log data they describe has been deleted by retention.

flowchart TD
    A["Producer writes to partition"] --> B["Entry created or refreshed with last write time"]
    B --> C{"No writes for longer than expiry period?"}
    C -->|"No"| B
    C -->|"Yes"| D["Entry removed from broker memory"]
    D --> E["Producer returns later with same PID"]
    E --> F{"Sequence is zero?"}
    F -->|"Yes"| G["Treated as a fresh producer"]
    F -->|"No"| H["Unknown producer error, client must recover"]
        
FIG 11 — Expiry of idle producer state and what happens when the producer returns
!
Common Mistake

Assuming a producer that has been quiet for a long time can simply resume. After its state expires, the broker no longer knows the PID, and the client will face an error unless it starts numbering from zero again. Recent clients handle this by requesting a new epoch automatically, but the situation still shows up in logs and metrics.

Memory cost

Each entry is small, only a handful of numbers and five compact batch summaries, but the total count is the number of producers multiplied by the number of partitions they touch. A cluster with a large number of short-lived producers writing to many partitions can accumulate a surprising amount of state, which is why controlling producer churn is a recurring operational theme.

15Leader Failover and Replication

The guarantee must survive the death of the very broker that holds the notebook. Here is how.

Followers keep their own notebooks

Follower replicas copy batches from the leader byte for byte, including the header that holds the PID, epoch, and sequence numbers. As each batch is appended to a follower’s log, the follower also updates its own producer state. So at any moment, every in-sync replica holds a nearly identical notebook, differing only by the last few batches still being copied.

What happens at failover

Suppose the leader dies just after storing a batch and just before acknowledging it. A follower that had already copied the batch becomes the new leader. The producer, having no acknowledgement, retries. The retry arrives at the new leader, which recognizes the numbers from its own notebook, and skips the append. There is no duplicate, even though the broker that first stored the batch is gone.

sequenceDiagram
    participant P as Producer
    participant L1 as Old leader
    participant L2 as Follower who becomes leader
    P->>L1: Batch with sequences 50 to 54
    L1->>L1: Append batch, update notebook
    L2->>L1: Fetch, copies batch and updates own notebook
    L1--xP: Leader crashes before acknowledging
    Note over L2: Elected new leader
    P->>L2: Retry same batch, sequences 50 to 54
    L2->>L2: Notebook shows this batch already stored
    L2-->>P: Success with original offsets
        
FIG 12 — A retry after leader failure is recognized by the new leader

The acknowledgement setting matters

The story above assumes the follower had received the batch before the leader failed. Idempotence therefore requires the strongest acknowledgement setting, in which the leader answers success only after all in-sync replicas hold the batch. With weaker settings, the leader might acknowledge a batch that no follower has yet copied. If that leader then dies, the batch is lost, and the producer, which thinks it succeeded, will have sent sequence numbers that the new leader has never seen. This mismatch can surface as an out-of-order error or as silent data loss.

With the strongest acknowledgements

  • Acknowledged batches exist on every in-sync replica
  • A new leader inherits the notebook for those batches
  • Retries after failover are correctly recognized
  • Combines well with a minimum in-sync replica setting

With weak acknowledgements

  • Leader may acknowledge before followers copy
  • Failover can lose acknowledged data
  • Producer and new leader disagree about sequence numbers
  • Idempotence cannot be safely enabled
i
Design note

This is why the client refuses to turn on idempotence when a weaker acknowledgement level has been explicitly requested. Deduplication is only meaningful if acknowledged data is durable across leader changes.

Unclean leader election

If the cluster allows a replica that was out of sync to become leader, it may lack batches that the previous leader had acknowledged. The producer’s next batch then has a base sequence that is higher than that leader expects, and the leader rejects it. Recent clients treat this as a recoverable problem in many cases, but it is a sign that data has been lost, and it deserves investigation.

16Errors and Recovery

Knowing the names of the errors, and what each one means, makes log messages far easier to read.

Error

Out of order sequence

The batch’s numbers do not fit the broker’s notebook. Either a batch is missing, or the broker lost state. The producer usually retries earlier batches first.

Error

Unknown producer

The broker has no notebook entry for this PID, often because the entry expired or the log data was deleted by retention.

Error

Invalid producer epoch

The producer holds an older epoch than the broker knows about, meaning a newer holder of that identity exists and this one is fenced.

Error

Timeout on delivery

The producer’s overall time limit for delivering a batch expired before it could be confirmed, even though retries were allowed.

How the client reacts

Retriable problems

Temporary issues such as a leader election or a network drop are simply retried with identical numbers, up to the delivery time limit. This is the everyday case, and the application never notices.

Sequence problems after a timeout

If a batch expires while it might still have reached the broker, the producer cannot know whether the broker stored it. Newer clients then request a fresh epoch and restart numbering, accepting the small risk that a previously sent copy may already exist, in exchange for keeping the producer usable.

Fencing

A fenced producer knows a newer instance has taken over. It cannot continue, and the application should stop that instance, since continuing would risk conflicting writes.

flowchart TD
    A["Produce request fails"] --> B{"Error kind"}
    B -->|"Temporary network or leader change"| C["Retry with identical numbers"]
    B -->|"Out of order sequence"| D["Resend earlier batches in order, or bump epoch if state was lost"]
    B -->|"Unknown producer"| E["Request new epoch and restart numbering"]
    B -->|"Invalid epoch, fenced"| F["Stop the producer, a newer instance owns the identity"]
    B -->|"Delivery time limit exceeded"| G["Fail the batch to the application, bump epoch if needed"]
    C --> H["Success or eventually one of the other branches"]
        
FIG 13 — How a producer reacts to different failures
!
Application responsibility

When the producer reports that a send finally failed, the application must decide what to do. Resending the record from application code creates a brand new record with new numbers, which the broker cannot recognize as a duplicate. Duplicates created that way are the application’s doing, not the producer’s.

17Configuration and Defaults

A handful of settings decide whether idempotence is on and how it behaves.

SettingRoleRequirement for idempotence
enable.idempotenceTurns the mechanism on or offOn by default in recent client releases
acksHow many replicas must hold a batch before acknowledgementMust be the strongest level, all in-sync replicas
retriesHow many times a failed send is retriedMust be greater than zero, default is effectively unlimited
max.in.flight.requests.per.connectionOutstanding requests per broker connectionMust be five or fewer
delivery.timeout.msOverall time budget for delivering a batch, including retriesBounds how long a retry loop may continue
transactional.idNames a transactional producerImplies idempotence, and adds stable identity
min.insync.replicas (topic or broker)Minimum replicas that must be in sync to accept writesNot required, but strongly advised alongside the strongest acknowledgements

What happens with conflicting settings

If you explicitly request idempotence together with an incompatible value, such as a weaker acknowledgement level, the client raises a configuration error at startup. If you leave idempotence unset and choose a conflicting value, recent clients quietly fall back to non-idempotent behavior rather than fail. This silent fallback is a classic trap, because an application may believe it has protection when it does not. Always verify the effective configuration in the startup logs of your producers.

!
Silent downgrade

A single copy-pasted configuration line that sets a weaker acknowledgement level can turn idempotence off without any loud complaint. Check the logged producer configuration after every change, and consider setting idempotence explicitly to true, so that a conflict becomes an error instead of a silent downgrade.

Older client versions

In older releases idempotence was off by default and had to be enabled by hand. Applications upgraded to newer clients may therefore change their behavior after the upgrade, gaining protection they did not previously have. Read release notes when moving between major versions, and re-test producer behavior under failure.

18What Idempotence Does Not Protect

A guarantee is only useful when you know its boundaries. These are the most important ones.

LIMIT-01 Boundary
Situation

A plain idempotent producer crashes after sending a batch but before receiving the acknowledgement. The application restarts and sends the same business event again.

Why It Is Not Covered

The restarted producer is given a brand new PID, and its numbering starts from zero. The broker sees a totally unrelated producer and stores the batch. No notebook entry links the two lives of the application.

What To Do

Use a transactional ID for a stable identity, or make consumers tolerate duplicates by carrying a business key that lets them detect repeats.

LIMIT-02 Boundary
Situation

The application itself calls send twice for the same business event, for example after a user double-clicks a button.

Why It Is Not Covered

Each call is a separate record with its own sequence number. From the broker’s view, they are two legitimate, different messages.

What To Do

Deduplicate at the source with a unique request identifier, or design consumers to be idempotent themselves.

LIMIT-03 Boundary
Situation

A producer writes to several partitions or topics, and a failure leaves some writes done and others not.

Why It Is Not Covered

Sequence numbers are tracked separately per partition. Idempotence guarantees that each partition sees each batch once, but it offers no all-or-nothing guarantee across partitions.

What To Do

Use transactions when writes across partitions must be atomic.

LIMIT-04 Boundary
Situation

A consumer reads a message, updates a database, and crashes before committing its position. After restart it reads the message again.

Why It Is Not Covered

The producer’s guarantee ends at the log. The consumer side has its own retry behavior, and reprocessing is a separate topic.

What To Do

Make consumer processing idempotent, or use transactions that tie consumed positions and produced results together.

i
One sentence to remember

Idempotence stops the producer’s own retries from creating duplicates within one producer life and one partition. It does not stop the application, the network of consumers, or a restarted producer from creating a second copy of the same business event.

19From Idempotence to Transactions

Transactions build on the same three ideas: identity, epoch, and sequence numbers.

What transactions add

A transactional producer receives a name, the transactional ID, chosen by the application. A dedicated broker role, the transaction coordinator, maps that name to a PID and keeps the mapping in an internal log. When the application restarts with the same name, it gets the same PID and a higher epoch. This gives a stable identity across restarts, and it fences older instances.

CapabilityIdempotent onlyTransactional
Dedup of retries within a sessionYesYes
Stable identity across restartsNoYes
Fencing of zombie instancesNot neededYes, through epochs
Atomic writes across partitionsNoYes
Consume, process, and produce atomicallyNoYes, with offsets included in the transaction
Extra costVery smallCoordinator round trips and markers
flowchart TD
    A["Idempotence: PID and sequence numbers"] --> B["Add a named identity: transactional ID"]
    B --> C["Stable PID, epoch increases on each restart"]
    C --> D["Zombie instances are fenced"]
    C --> E["Group writes into transactions"]
    E --> F["Commit or abort markers written to each partition"]
    F --> G["Consumers can read only committed data"]
        
FIG 14 — Transactions layered on top of idempotence
Simple Analogy

Idempotence is the tracking slip that stops the sorting office from processing the same parcel twice. Transactions add a signed delivery receipt covering several parcels together, so that either all of them are delivered and confirmed, or none of them count.

Idempotence is always inside

A transactional producer cannot work without idempotence. Every write inside a transaction still carries a PID, epoch, and sequence number, and the broker still validates them in exactly the way described earlier. Transactions simply add markers and a coordinator on top. Understanding the idempotent producer thoroughly is therefore the best preparation for understanding transactions.

20A Worked Example With Real Numbers

Abstract rules become easy once you watch the numbers move through a small, concrete story.

The setting

A payment service uses one idempotent producer. The cluster hands it PID 5001 with epoch 0. The service writes to a topic with three partitions, so the producer keeps three independent counters, all starting at zero. The leader of partition 1 is broker A, and broker B holds a follower replica of partition 1.

Round one: a normal batch

The producer collects four records for partition 1. It stamps the batch with PID 5001, epoch 0, base sequence 0, and a count of four. Broker A sees an unknown producer with base sequence zero, which is the legal way to begin. It appends the batch at log offsets 900 to 903 and creates a notebook entry that says: producer 5001, epoch 0, last sequence 3, last offset 903. Broker B copies the batch and builds the same entry.

StepBatch stampBroker A notebook after the stepOutcome
1PID 5001, epoch 0, base 0, 4 recordsLast sequence 3, last offset 903Stored at offsets 900 to 903
2PID 5001, epoch 0, base 4, 2 recordsLast sequence 5, last offset 905Stored at offsets 904 and 905
3Same as step 2, retried after lost replyUnchanged: last sequence 5, last offset 905Duplicate skipped, success returned with offsets 904 and 905
4PID 5001, epoch 0, base 9, 3 recordsUnchangedRejected: expected base 6, got 9
5PID 5001, epoch 0, base 6, 3 recordsLast sequence 8, last offset 908Stored at offsets 906 to 908
6Retry of step 4, now with base 9Last sequence 11, last offset 911Stored at offsets 909 to 911

What the table teaches

Step three shows deduplication: the broker recognized the batch, did not add it again, and returned the offsets of the first copy. Step four shows gap detection: a later batch arrived early and was refused. Step five shows the missing batch arriving, and step six shows the earlier rejected batch succeeding once its predecessor landed. The final log holds every record exactly once and in the intended order, despite one lost reply and one overtaking batch.

sequenceDiagram
    participant P as Producer PID 5001
    participant A as Broker A (leader)
    P->>A: Base 0, 4 records
    A-->>P: Stored at 900 to 903
    P->>A: Base 4, 2 records
    A->>A: Stored at 904 and 905
    A--xP: Reply lost
    P->>A: Base 9, 3 records (sent while waiting)
    A-->>P: Rejected, expected base 6
    P->>A: Retry base 4, 2 records
    A-->>P: Duplicate, success with 904 and 905
    P->>A: Base 6, 3 records
    A-->>P: Stored at 906 to 908
    P->>A: Retry base 9, 3 records
    A-->>P: Stored at 909 to 911
        
FIG 15 — Lost reply, overtaking batch, and recovery in one story

Now add a failover

Imagine broker A crashes right after step five, before acknowledging it. Broker B, which had copied that batch, becomes leader. Its notebook says the last sequence is 8. The producer retries the batch with base 6, and broker B recognizes it, returning success without a second copy. Next the producer sends base 9, which is exactly the expected next value, and the story continues as if nothing happened.

Now add a producer restart

Suppose the payment service restarts. The new producer asks for identity and receives PID 5002. Its counters start at zero. If it now resends a payment event that the old producer already wrote, the broker sees producer 5002 with base sequence zero, which is perfectly valid, and stores it. Two copies exist. This is the boundary described earlier, made visible in numbers: the notebook has entries for 5001 and 5002, and nothing links them.

!
Lesson

The protection is strong inside a producer’s life and silent across lives. If a business event might be resent by a new producer instance, give that event a unique business key and make the consumer check it, or use a transactional ID.

21Client Internals: Accumulator, Sender, and Recovery Logic

Inside the producer library, a few carefully arranged rules keep the sequence numbers honest.

Numbers are assigned late

Records sit in the accumulator for a short time before they are drained into a request. Sequence numbers are assigned when a batch is drained for its first send, not when the record is first handed to the producer. This ordering matters: the number a batch receives reflects the order in which batches actually leave for the partition, which is the order the broker will validate.

Keeping retries in order

When a batch must be retried, the producer places it back at the front of the queue for its partition. Batches with earlier sequence numbers are always sent before batches with later ones. If it did not do this, its own retries would keep colliding with the broker’s gap checks. The sender also tracks, per partition, the highest sequence number that has been acknowledged, so it knows which in-flight batches are safe to forget.

flowchart TD
    A["Batch drained from accumulator"] --> B["Assign base sequence from partition counter"]
    B --> C["Send to leader, remember as in flight"]
    C --> D{"Reply"}
    D -->|"Success"| E["Remove from in flight, advance acknowledged sequence"]
    D -->|"Retriable error"| F["Put batch back at front of partition queue, keep same numbers"]
    F --> C
    D -->|"Delivery time limit reached"| G["Fail batch, decide about epoch bump"]
    G --> H["Later batches for this partition are reset with new epoch and fresh numbers"]
        
FIG 16 — Lifecycle of a batch inside the producer

When a batch cannot be confirmed

The trickiest moment is when a batch has used up its delivery time and the producer still does not know whether the broker stored it. The producer cannot simply forget the batch and move on, because later batches have already been numbered on the assumption that this one exists. If the broker never received it, those later batches would now show a gap. The producer therefore has two choices: keep trying until it learns the answer, or bump the epoch and renumber everything that follows from zero. Recent clients choose the second path, since it lets the producer continue, and they accept a small, understood risk in exchange.

Why bumping the epoch is safe

A new epoch means the broker discards its old notebook entry for that PID and starts fresh. All later batches begin at sequence zero, and no gap can arise. The cost is that the broker can no longer tell whether the unconfirmed batch was stored before, so in rare cases a copy might exist. The producer reports the expired batch as failed to the application, so the application is aware that its status is uncertain.

Who coordinates the bump

The client asks the cluster for a new epoch for its existing PID. For transactional producers, the transaction coordinator handles it. For plain idempotent producers, recent versions support the same idea through the identity request, so the producer keeps its PID but gets a higher epoch.

Simple Analogy

A courier numbers a series of parcels 1 to 10. Parcel 4 vanishes and nobody can tell whether it reached the depot. Rather than leaving a hole in the series that would block all later parcels, the courier starts a new series with a new colored label, and notes that parcel 4’s fate is unknown.

Partition changes and numbering

The counters are per partition, so when a topic gains partitions, the producer simply begins a new counter for each new partition it writes to. Changing which partition a given key maps to does not disturb existing counters. The producer’s memory of counters grows with the number of distinct partitions it writes to, which is another reason to keep the count of partitions per producer reasonable.

i
What the application sees

All of this recovery happens inside the client library. The application sees either normal success, or a failure callback for a specific record. It should treat the failure callback as “outcome uncertain” when the reason is a timeout, and design its next step accordingly.

22Idempotence Compared With Other Deduplication Strategies

Idempotent producers are one tool among several. Knowing the others helps you pick the right mix.

StrategyWhere it actsStrengthWeakness
Idempotent producerBroker, per producer session and partitionAutomatic, cheap, covers retry duplicates and orderDoes not span restarts, partitions, or application resends
TransactionsBroker and coordinator, across partitions and restartsAtomic multi-partition writes and stable identityMore moving parts and higher latency
Consumer-side business key checkConsumer or downstream storeHandles duplicates from any originNeeds storage of seen keys and code in every consumer
Database unique constraintTarget databaseVery strong and simple for sinks that support itOnly helps where the sink can enforce uniqueness
Log compaction by keyTopic retentionKeeps only the latest value for each key over timeDuplicates remain visible until compaction runs
Outbox patternApplication databaseTies event publishing to database changes safelyRequires a relay process and still needs downstream dedup

Choosing a combination

Combination 1

Idempotent producer plus consumer keys

The most common and practical mix. Retry duplicates disappear at the source, and any remaining repeats are caught downstream by a business key.

Combination 2

Transactions plus read-committed consumers

For pipelines that read, process, and write within Kafka and need atomic results. Adds stable identity, fencing, and all-or-nothing behavior.

Combination 3

Outbox plus idempotent producer plus unique keys

For services that must publish an event whenever their own database changes. The outbox makes publishing reliable, the producer removes retry duplicates, and unique keys catch any remaining repeats.

Combination 4

Idempotent producer plus compaction

For topics that represent the latest state of an entity, where the newest value for a key is all that matters over time.

“Removing duplicates is like keeping a house tidy: one big cleanup at the door is far easier than chasing crumbs in every room.”

Where to put the effort

The closer to the source you remove a duplicate, the cheaper it is, because fewer systems ever see it. That is the argument for idempotent producers: they stop the most common duplicates before they enter the log. Yet duplicates from application logic, restarts, or consumer reprocessing sit outside the producer’s view, so a prudent design adds a second, downstream line of defense for the data that truly matters, such as money.

i
Practical guidance

Ask, for each topic: what does a duplicate cost? For log lines, almost nothing, and the default idempotent producer is plenty. For payments, quite a lot, and a business key check is worth building even though idempotence is on.

23How the Feature Evolved Across Kafka Versions

Knowing the history explains many quirks you will meet in older clusters and older documentation.

1

The original design (2017)

Idempotent producers and transactions arrived together in the 0.11 release, along with the new record batch format that carries the PID, epoch, and sequence fields in the header. The feature was off by default and had to be enabled by hand.

2

Smarter recovery (2020)

A later improvement let producers bump their epoch after certain errors, rather than becoming permanently unusable. This made idempotent producers far more forgiving of expired state and unclear delivery outcomes.

3

Idempotence on by default (2021)

With the 3.0 release, the client turned the feature on by default, together with the strongest acknowledgement setting, because the cost was so small and the protection so valuable. The same release added tooling for inspecting active producers on a partition.

4

Separate expiry for producer state (2023)

Later releases separated the expiration of plain producer state from that of transactional identities, so the two could be tuned independently and so that state from idempotent producers could be cleaned more precisely.

5

Protection against identity floods (2024 onward)

Newer releases added ways to limit how many new producer identities a client may create in a period, reflecting real incidents in which clients that created producers in tight loops overloaded brokers.

6

The metadata quorum era

As clusters moved from an external coordination service to the built-in controller quorum, the allocation of producer ID blocks moved to the controller as well, with the same guarantee that no ID is given out twice.

!
Check your versions

Exact behavior of defaults, tooling, and limits depends on the version of both the client library and the brokers. Always confirm against the release notes for your specific versions before relying on a detail from this tutorial in production.

What changed for you as a user

From opt-in to opt-out

Applications written years ago often set idempotence by hand. Newer applications get it for free. A codebase that mixes both styles may have producers with different behavior, so audit the effective settings across services.

From fatal errors to recoverable ones

Situations that once forced an application restart, such as an expired producer entry, are now often handled inside the client. Error handling code written for old versions may be more defensive than necessary.

From invisible to inspectable

With tooling to list producers per partition, questions like “who is writing to this partition, and when did they last write?” can now be answered directly, which turns many mysteries into short investigations.

24Performance and Resource Costs

Does this safety come at a price? The honest answer is yes, but the price is very small.

Costs on the producer

The producer keeps a counter per partition and a little bookkeeping for batches that are in flight. It must hold on to each unacknowledged batch until confirmed, so that retries can use the identical numbers. These items use a small amount of memory and negligible processing time.

Costs on the wire and on disk

Each batch header carries fourteen extra bytes for the PID, epoch, and base sequence. Since a batch typically holds many records, the added weight per record is tiny. These fields are present in the modern format whether or not idempotence is turned on, so switching the feature on adds nothing further to the stored data.

Costs on the broker

Cost

Producer state memory

One small entry per producer per partition replica, with five batch summaries each. Grows with the number of active and recently active producers.

Cost

Snapshot files

Small files written at segment rolls, and read on restart. Cheap in normal cases, larger when the producer count is enormous.

Cost

Validation work

A handful of integer comparisons per batch. Effectively free next to disk and network work.

Cost

Identity requests

One request per new producer instance. Trivial for long-lived producers, noticeable when producers are created and dropped at very high rates.

14 bytes
Extra header size per batch for identity fields
5 batches
Remembered per producer and partition
1 request
Needed once per producer start for identity

The real performance trap: producer churn

The overwhelming majority of idempotence-related production problems come from one habit: creating a new producer object for every request, or for every small unit of work. Each new producer asks for a new PID, then creates fresh state on every partition it writes to. If a service creates thousands of such producers per second, the cluster accumulates a heap of short-lived identities. Brokers spend time answering identity requests and storing state that will expire unused, and memory usage climbs.

!
Design rule

A producer is designed to be long-lived and shared between threads. Create it once per application instance, keep it for the life of the process, and close it on shutdown. Newer releases also give operators tools to limit the rate at which one client can create new identities, precisely because of this problem.

flowchart LR
    subgraph Bad["Producer per request"]
        R1["Request 1 creates producer"] --> P1["New PID"]
        R2["Request 2 creates producer"] --> P2["New PID"]
        R3["Request 3 creates producer"] --> P3["New PID"]
        P1 --> S["Thousands of short-lived state entries on brokers"]
        P2 --> S
        P3 --> S
    end
    subgraph Good["Shared long-lived producer"]
        R4["Requests 1 to N"] --> P4["One producer, one PID"]
        P4 --> T["Small stable state on brokers"]
    end
        
FIG 17 — Why producers should be long-lived and shared

Throughput effects of the in-flight limit

Because idempotence caps in-flight requests at five, a very high-latency link between producer and broker can limit throughput. In such cases, larger batches and moderate delays to fill them are more effective than trying to push more requests in flight.

A back-of-envelope sizing exercise

Numbers make the memory cost of producer state easy to feel. Suppose a partition replica has entries for producers that wrote to it during the last expiry period. Each entry holds a producer id, an epoch, a last sequence, a last offset, timestamps, and up to five small batch summaries. Even with generous rounding, an entry is a few hundred bytes. Now multiply by the number of distinct producers and partitions.

ScenarioActive producers per partitionPartitions per brokerEntries on the brokerRough meaning
Small service fleet105005,000Negligible memory
Busy platform2002,000400,000Tens of megabytes, comfortable
Producer per request pattern50,0002,000100,000,000Gigabytes of state, snapshot files swell, restarts slow down

The lesson from the table is not that idempotence is heavy. The first two rows show it is almost free. The third row shows how one bad habit multiplies a tiny cost by a giant factor. Bounding the count of producers is therefore a better lever than trying to shave bytes from each entry.

i
Planning tip

When capacity planning a cluster, estimate the number of long-lived producer instances across all your services, multiply by the average number of partitions each one writes to, and watch that product over time. A sudden jump is a much earlier warning than any memory alarm.

25Security Considerations

Identity systems attract attention from attackers and from accidents alike.

Can someone forge a PID?

Producer identities are assigned by the cluster, and the client cannot choose one. A malicious client could try to send batches stamped with a PID it observed elsewhere, but in a secured cluster every connection is authenticated, and every write is authorized against the topic. The PID adds no permission by itself. An attacker who already has write permission to a topic can write to it with or without a PID, so the identity mechanism does not widen access.

Control 1

Authenticate every client

Use TLS client certificates or SASL, so that every producer connection is tied to a known principal.

Control 2

Authorize writes

Grant write permission per topic to specific principals. In older versions, an additional cluster-level permission for idempotent writes was also needed when access control was enabled.

Control 3

Protect transactional IDs

Transactional producers need permission on their transactional ID as a resource. Without this, any authorized client could steal the identity of another application and fence it.

Control 4

Encrypt in transit

Use TLS so batches, including headers, cannot be captured or replayed by someone on the network.

Control 5

Limit identity creation

Apply quotas and limits on how quickly one client can create new producer identities, to prevent a misbehaving or malicious client from exhausting broker memory.

Control 6

Restrict log file access

The log and its producer state snapshots reveal which producers wrote what and when, so operating system permissions on them should be tight.

i
Replay attacks

Idempotence gives a small side benefit: a replayed batch captured from the network, even if an attacker could inject it, would be recognized by its sequence numbers and not stored a second time within the window. Encryption and authentication remain the real defense, but the sequence check adds a layer.

!
Common Mistake

Sharing one transactional ID across different applications or environments. The second application to start will fence the first, and both may fail in confusing ways. Give every logical producer instance a unique, well-managed transactional name.

26Monitoring, Logging, and Metrics

When idempotence works, nothing visible happens. That is why you need signals that show it working and warn when it is not.

On the producer

SignalWhat it showsHealthy pattern
Record retry rateHow often records are being resentLow and flat, spikes match known incidents
Record error rateRecords that failed for goodZero or near zero
Request latencyRound trip time to brokersStable, no upward drift
Batch size and records per requestEfficiency of batchingSteady, near your configured targets
Buffer availabilityWhether the producer is blocked by full buffersComfortable headroom
Startup configuration logThe effective settings, including idempotenceIdempotence shown as enabled

On the broker

Producer state size

Watch how many producer entries partitions hold, and how quickly that number grows. A sudden rise is a strong sign of producer churn or a runaway client.

Error counts by type

Track counts of out-of-order sequence, unknown producer, and invalid epoch errors. A few are normal around failovers. A steady stream indicates a real problem.

Produce request latency and failed produce rate

Standard health indicators that also reflect how often producers are forced to retry.

Under-replicated partitions and leader elections

These events cause retries and can trigger sequence errors, so correlate producer errors with replication health.

Tools that ship with Kafka

Tool

Log dump tool

Prints batch headers, including producer ID, epoch, and base sequence, straight from log segment files, and can also print the contents of producer state snapshots.

Tool

Transactions tool

Can list active producers on a partition, showing their IDs, epochs, last sequence numbers, and last write times, and can describe transactional identities.

Tool

Consumer and topic tools

Help verify that record counts match expectations after a fault test, by comparing what was sent with what landed.

i
Fault-injection testing

The best proof that idempotence works in your setup is a test: send a known number of unique records, deliberately restart a broker or drop network traffic mid-send, and count the records afterward. Any surplus means something in your configuration or application is bypassing the protection.

flowchart TD
    A["Alert: rising out-of-order or unknown producer errors"] --> B{"Recent broker restarts or leader changes?"}
    B -->|"Yes"| C["Expected in small numbers, check they settle"]
    B -->|"No"| D{"Producer count growing fast?"}
    D -->|"Yes"| E["Find the client creating many short-lived producers"]
    D -->|"No"| F{"Producers idle for longer than expiry?"}
    F -->|"Yes"| G["Producers resume after expiry, allow client recovery or shorten idle gaps"]
    F -->|"No"| H["Check for unclean elections and lost data on the partition"]
        
FIG 18 — A triage path for sequence-related errors

Building a small lab to prove it works

Trust in a guarantee grows fastest when you have watched it survive abuse. A small test environment with three brokers is enough. The plan is simple: run a producer that sends a large, known number of unique records, each carrying a serial number in its value. While it runs, cause trouble: restart the leader of the partition, pause and resume a broker’s process, or add artificial network delay and packet loss between producer and broker. When the run ends, read the partition from the beginning and check two things: that every serial number from the run appears, and that none appears more than once.

1

Baseline without trouble

Send the records with no faults, and confirm the count matches exactly. This proves your counting method is sound.

2

Faults with idempotence off

Repeat with the feature deliberately disabled, and observe duplicates or reordering. Seeing the problem first makes the fix meaningful.

3

Faults with idempotence on

Run the same faults again with the feature enabled, and confirm that duplicates vanish while producer retry metrics show that retries did occur.

4

Restart the producer mid-run

Kill and restart the producer process partway through and resend the last unconfirmed records. This shows the boundary: duplicates can now appear, and your business-key check should catch them.

i
Look at the headers

After the run, use the log dump tool on the partition’s segment files and look at the producer id, epoch, and base sequence of neighboring batches. Watching consecutive sequence numbers line up, and noticing where a new producer id appears after a restart, turns the theory of this tutorial into something you can point at.

27Design Patterns and Anti-patterns

Habits that repeatedly work well, and habits that repeatedly cause trouble.

Patterns worth copying

One long-lived producer per application instance

Create it at startup, share it across threads, and close it at shutdown. This keeps identity stable and broker state small.

Idempotent producer plus idempotent consumer

Use the producer feature to remove duplicates that come from retries, and add a business key check in the consumer to handle duplicates that originate elsewhere. Two thin layers together cover far more ground than either one alone.

Strong durability settings alongside idempotence

Pair the strongest acknowledgements with a sensible minimum of in-sync replicas and a replication factor of three, so acknowledged data survives failovers and the sequence bookkeeping stays consistent.

Bounded delivery time

Set a delivery time limit that matches your business tolerance, so a stuck send fails visibly rather than retrying for a very long time in silence.

Failure drills

Regularly test with broker restarts and network faults, counting records to confirm no duplicates or losses occurred.

Anti-patterns to avoid

ANTI-PATTERN-01 Avoid
Problem

A web service creates a new producer for each incoming HTTP request, sends one message, and closes the producer.

Why It’s Harmful

Each request triggers an identity request, creates state on the partition leader, and leaves expiring entries behind. Latency grows, brokers spend memory on useless state, and the identity counter and coordinator are stressed.

Correct Approach

Create one producer when the service starts and share it. Producers are thread-safe and designed for exactly this use.

ANTI-PATTERN-02 Avoid
Problem

An application catches a send failure and blindly resends the same business event from its own code, several times, to “make sure” it arrives.

Why It’s Harmful

Each application-level resend is a new record with new sequence numbers. The broker cannot recognize it as a duplicate, so the very duplicates idempotence was meant to prevent are created by the application itself.

Correct Approach

Let the producer’s built-in retries do the work. If a send finally fails, handle it deliberately, for example by writing the event to a durable outbox and sending it with a business key that consumers can use to spot repeats.

ANTI-PATTERN-03 Avoid
Problem

Configuration copied from an old project sets weak acknowledgements and disables retries, while the team believes idempotence is protecting them.

Why It’s Harmful

Idempotence is silently off. Data can be lost on failure, and duplicates can occur if retries are later re-enabled by other means.

Correct Approach

Set idempotence explicitly, use the strongest acknowledgements, keep retries enabled, and verify the effective settings in the startup log.

ANTI-PATTERN-04 Avoid
Problem

A team calls idempotence “exactly once” in design documents and skips consumer-side protection entirely.

Why It’s Harmful

Producer restarts, application resends, and consumer reprocessing all remain possible sources of duplicates. Downstream systems built on this false assumption fail in ways nobody expected.

Correct Approach

Document the exact boundaries of the guarantee, and add business-key deduplication or transactions where end-to-end exactness is truly needed.

28Best Practices and Common Mistakes

A checklist you can carry into design reviews and code reviews.

Best practices

  • Keep idempotence enabled, and set it explicitly to catch conflicts
  • Use the strongest acknowledgements and a replication factor of three
  • Share one long-lived producer per application instance
  • Keep retries enabled and bound them with a delivery time limit
  • Use a transactional ID when identity across restarts or atomic multi-partition writes are needed
  • Add business keys so consumers can detect repeats from any source
  • Run fault-injection tests and count the results

Common mistakes

  • Creating a producer per request or per small task
  • Believing idempotence survives producer restarts
  • Resending failed records from application code
  • Mixing weak acknowledgement settings with idempotence and not noticing the silent downgrade
  • Reusing one transactional ID across different applications
  • Ignoring rising producer-state counts on brokers
  • Assuming consumers never see repeats

A short design review checklist

1

Identify the failure you fear

Is it retries within a session, restarts of the application, or reprocessing by consumers? Each needs a different tool.

2

Choose the layer

Plain idempotence for retries, transactions for identity and atomicity, consumer-side keys for everything else.

3

Set and verify the configuration

Confirm in logs that idempotence is truly active and that no conflicting setting overrides it.

4

Test with faults

Restart brokers and drop connections during a send, and confirm that record counts match exactly.

5

Monitor continuously

Watch retry rates, error types, and producer-state counts so that drift is noticed early.

A tuning cheat sheet for common workloads

Different workloads stress idempotence in different ways. The guidance below is a starting point, not a law, and each row should be confirmed against measurements from your own system.

WorkloadMain concernSensible approach
High-volume logs and metricsThroughput, cheap duplicatesKeep defaults, use larger batches with a short delay, compress, and rely on idempotence alone
Payments and financial eventsCorrectness above speedStrongest acknowledgements, replication factor of three, a sensible minimum of in-sync replicas, transactional IDs where needed, and business keys in every event
Bursty web trafficProducer churn and latencyOne shared long-lived producer per application instance, never one per request
Mobile or edge gateways on flaky linksFrequent retries and long delaysA generous delivery time limit, moderate batch sizes, and monitoring of the retry rate
Stream processing jobsAtomic read-process-writeTransactions with stable transactional IDs, one per task, and read-committed consumers downstream
Rarely active batch jobsState expiry between runsExpect a fresh identity each run, and let the client handle any expired state automatically

Reading the retry rate correctly

A nonzero retry rate is not a defect. It means the machinery is doing its job: transient trouble happened and was absorbed. What matters is the trend. A flat, low retry rate with zero final errors is healthy. A retry rate that climbs steadily points to a slowing broker, a saturated network, or an overloaded partition, and the idempotent producer is quietly hiding the symptoms from your application. Treat the retry metric as an early warning light, because it often rises hours before any user-visible failure appears.

Choosing delivery time limits with care

The delivery time limit is the total budget for one batch, including waiting in the buffer, every network attempt, and every retry pause. If it is too short, healthy batches will expire during a brief leader election, forcing epoch bumps and uncertainty for no reason. If it is too long, a genuinely stuck send hangs around for minutes, holding memory and blocking later batches for the same partition. A good starting point is a value comfortably longer than the slowest leader election you have observed, and shorter than the time after which your business would rather see a failure than wait.

!
Avoid extremes

Setting the delivery time limit extremely low to “fail fast” causes exactly the unclear outcomes that sequence numbers are meant to avoid. Setting it extremely high hides real outages. Aim for a value that reflects your actual recovery times, and revisit it after each incident.

29Real-World and Industry Examples

The same mechanism shows up in very different businesses, each with its own reasons to care.

Payments and banking events

A payment service publishes “payment captured” events. A retry after a network stall must not create a second event, or downstream ledgers and notifications will double count. Idempotent producers remove the retry duplicates, while a payment reference number in each event lets consumers guard against duplicates from every other source.

E-commerce order pipelines

Order creation, inventory reservation, and shipping messages travel through several topics. Preserved order within a partition, guaranteed even when retries occur, helps ensure that a cancellation never overtakes the order it cancels.

Change data capture streams

Tools that stream database changes into Kafka depend on order and on not repeating changes. Idempotent sending stops a retry from reapplying an update, which keeps downstream replicas consistent with the source.

Metrics and telemetry ingestion

Agents on thousands of servers push counters into Kafka. Duplicate points inflate totals, so removing duplicates at the producer level keeps aggregates trustworthy without expensive deduplication later.

Ride-hailing and logistics tracking

Location updates and trip state changes arrive in bursts, often from mobile networks that drop and reconnect. The backend producers that publish them onward benefit from safe retries and consistent order per trip key.

Stream processing frameworks

Frameworks that offer exactly-once processing rely on transactional producers, which in turn rest on the identity, epoch, and sequence design described here. The guarantees they advertise are only as strong as this foundation.

NeedBest fitWhy
Safe retries in a long-running serviceIdempotent producerCheap, automatic, and covers the common failure
Survive application restarts without duplicatesTransactional IDStable identity and fencing
Write to several topics atomicallyTransactionsAll-or-nothing commit
Protect against user double submissionsBusiness key and consumer checkDuplicates originate outside Kafka
Read-process-write pipelinesTransactions with consumed offsetsTies input position and output together

30Frequently Asked Questions

Q1Is idempotence the same as exactly-once delivery?

No. Idempotence removes duplicates caused by the producer’s own retries within one producer session and one partition. Exactly-once across restarts, several partitions, and consumers needs transactions and careful consumer design.

Q2Does the broker really need to remember producers?

Yes. Without a small memory of recent sequence numbers per producer and partition, the broker would have no way of telling a retry from a new batch.

Q3Who chooses the PID, the client or the broker?

The cluster chooses it. The client asks for an identity, and receives one from a block of unused values held by a broker.

Q4What happens to my sequence numbers when I restart the application?

A plain idempotent producer gets a brand new PID and starts every partition’s counter at zero. The old numbers are irrelevant, because the broker treats the new PID as an unrelated producer.

Q5Why are only five batches remembered?

It keeps broker memory small, and it matches the cap of five requests in flight. Any batch that could be retried is always still within the remembered window.

Q6Does idempotence hurt throughput?

Only slightly, if at all. The extra work is a few integer comparisons and a small header, and the cap of five in-flight requests is rarely a limit in practice.

Q7What is the difference between the producer epoch and the partition leader epoch?

The producer epoch versions a producer identity and fences older holders of that identity. The leader epoch versions the leadership of a partition and fences older leaders. They serve similar purposes for different actors.

Q8Can two producers share the same PID?

Not at the same time in a healthy cluster. For plain idempotent producers, each instance gets its own PID. For transactional producers, two instances with the same transactional ID share a PID, but only one, the one with the higher epoch, is allowed to write.

Q9What if the broker’s producer state is lost?

It can be rebuilt from the batch headers in the log, using the last snapshot as a shortcut. Followers hold their own copies too, so a leader change does not lose the state.

Q10Why did my idempotent producer report an unknown producer error after a quiet period?

The broker discards state for producers that have been silent longer than the expiration period. When the producer wrote again, the broker no longer recognized its identity. Recent clients recover by asking for a new epoch.

Q11Do I need to change my code to use idempotence?

Usually not. In current client versions it is enabled by default. You mainly need to avoid configuration settings that conflict with it, and confirm the effective settings at startup.

Q12Does compression change how deduplication works?

No. Deduplication looks at the batch header fields, not the compressed record contents, so it works the same with any compression choice.

Q13Why must the strongest acknowledgement setting be used?

So that every acknowledged batch is present on all in-sync replicas. Then a new leader after failover already knows the sequence numbers, and retries are handled correctly.

Q14Can I see the PID and sequence numbers of my messages?

Yes, through the log dump tool, which prints batch headers from segment files. Regular consumers do not receive these fields as part of the records they read.

Q15Is idempotence enough for a consume-transform-produce job?

No. Such a job also needs its consumed positions tied to its produced output. Transactions do this, by committing the output and the input offsets together as one unit.

Q16What is a zombie producer?

An older instance of a transactional producer that is still running after a newer instance has taken over, perhaps after a long pause. Its lower epoch means brokers refuse its writes, so it cannot cause harm.

Q17Does idempotence work with a topic that has only one partition?

Yes, and it is the simplest case. The producer keeps one counter for that partition, and the leader keeps one notebook entry per producer. Everything works exactly as described, just with fewer counters.

Q18What happens if I change the number of partitions of a topic?

Existing counters continue unchanged, and the producer starts fresh counters for any new partitions it writes to. The guarantee stays intact, although the mapping of keys to partitions may change, which affects ordering by key rather than deduplication.

Q19Can a batch be partly duplicated?

No. A batch is accepted or treated as a repeat as a whole. The broker compares the full range of sequence numbers claimed by the batch, so there is no state in which half of a batch is stored and half is recognized as a repeat.

Q20Is the producer state replicated like normal data?

Indirectly. The state itself is not copied as a separate object. Instead, each follower rebuilds the same state from the batches it copies, because the batch headers carry all the needed numbers. Snapshots are local to each replica.

Q21Why do I see sequence errors right after a broker restart?

A few are normal while leadership moves and producers retry. If they continue for a long time, check for lost data on the partition, unclean leader elections, or producers whose state expired while they were idle.

Q22Do consumers ever see the sequence numbers or the PID?

Not in the record objects they receive. The values live in the batch header and are used by brokers, and by tools that read log files directly. Consumers only see the effect: no repeated retries in the data.

Q23Is it safe to reduce the idle expiry of producer state to save memory?

It reduces memory, but makes it more likely that a producer returning after a quiet gap finds its entry gone. Recent clients recover, yet each recovery involves an epoch bump and some uncertainty about the last unconfirmed batch. Shorten it only if your producers write frequently and your memory truly requires it.

Q24Should I still design consumers to tolerate duplicates?

For anything important, yes. Idempotent producers close the retry gap, but duplicates can still arrive from application resends, producer restarts, or consumer reprocessing. A business key check downstream is a cheap and durable second line of defense.

31Summary and Key Takeaways

The idempotent producer solves a specific, common problem: a retry that follows a lost acknowledgement must not leave a second copy in the log. It does so with three simple ingredients. A producer ID identifies who is writing, an epoch says which version of that identity is valid, and per-partition sequence numbers, carried in the header of every batch, say exactly which records a batch contains. The partition leader keeps a small notebook of recent numbers for each producer, rebuilds it from the log after restarts, and shares it with followers through ordinary replication, so failovers keep the protection. Understanding the precise edges of this guarantee lets you use it confidently, and add transactions or consumer-side keys where a larger promise is needed.

Key Takeaways

  • Identity plus numbering — the PID names the producer and the base sequence numbers the records, so the broker can tell a repeat from a new batch by reading the header.
  • Broker-side detection — the partition leader keeps recent sequence details for each producer, up to five batches, and skips repeats while returning the original offsets.
  • Gaps are rejected — a batch that skips ahead is refused, which preserves both completeness and order within the partition.
  • Epochs fence the past — a higher epoch always wins, so stale or zombie producers are blocked, and clients can bump the epoch to recover from lost state.
  • State survives failure — headers on disk, snapshots, and replication let restarts and leader changes keep the notebook, provided the strongest acknowledgements are used.
  • Boundaries are real — the guarantee covers one producer session and one partition. Restarts, application resends, multi-partition atomicity, and consumer reprocessing need transactions or business-level deduplication.
  • Keep producers long-lived — share one producer per application instance, verify settings at startup, monitor retries and producer-state growth, and test with injected faults.