Exactly-Once in Kafka Connect

Exactly-Once in Kafka Connect

Moving data between Kafka and other systems sounds simple until a crash lands in the wrong second. This guide explains what exactly-once really means for Connect, how source and sink connectors achieve it in very different ways, and where the promise stops.

Imagine paying a bill through an online bank. You press the pay button, the screen freezes, and you are not sure whether the payment went through. If you press again, you might pay twice. If you do not, you might pay nothing. A good bank designs its system so that it does not matter how many times you press, because the payment happens exactly once. Data pipelines face this same problem millions of times a day, and Kafka Connect is one of the places where it is solved.

This tutorial starts from the very basics of delivery guarantees and builds up to the transactional machinery that lets source connectors write each record exactly once. It also explains, honestly, why sink connectors need a different approach, and why the strongest promise in one part of a pipeline can be quietly cancelled by a weak link in another part.

1Delivery Guarantees From Scratch

Before talking about exactly-once, we need to know what the other options are and why they exist.

When a system moves a message from one place to another, three outcomes are possible if something can fail in the middle. The message may arrive zero times, once, or more than once. Engineers name the guarantees after what they promise, and each one is a trade between simplicity, speed, and correctness.

Guarantee 1

At-Most-Once

Each message is delivered zero or one time. It is never repeated, but it may be lost if a failure happens at the wrong moment.

Guarantee 2

At-Least-Once

Each message is delivered one or more times. Nothing is lost, but a retry after a failure can create duplicates.

Guarantee 3

Exactly-Once

Each message has its effect one time only. No loss and no visible duplicates, even when failures and retries happen underneath.

Simple Analogy

Think of sending a registered letter. At-most-once is dropping it in the mailbox and never checking. At-least-once is sending it again whenever you are unsure, so the recipient might get two copies. Exactly-once is sending it with a serial number, so the recipient’s office can throw away any copy it has already filed.

What Exactly-Once Really Means

The phrase is slightly misleading. In a network where messages can be delayed or resent, a message may physically be transmitted more than once. What exactly-once promises is about effects, not transmissions. The final result must look as if each message was processed one single time. The system achieves this by making repeated attempts harmless, through identifiers, transactions, or atomic updates.

!
Common Misconception

Exactly-once is not a magic switch that makes the whole universe consistent. It is a guarantee about a defined scope, such as a write into Kafka. Anything outside that scope, such as a database on the far side of a sink, needs its own protection.

2A Quick Tour of Kafka Connect

You need a clear mental picture of the moving parts before you can see where things go wrong.

Kafka Connect is a framework, shipped with Apache Kafka, for moving data between Kafka and other systems without writing custom applications. Instead of coding a producer or consumer for every database, file store, or search index, you configure a ready-made connector and let the framework run it.

A connector is the plug-in that knows how to talk to one specific system. A source connector reads from an outside system and writes into Kafka topics, for example reading changes from a database. A sink connector reads from Kafka topics and writes into an outside system, for example loading events into a data warehouse or a search index.

Part 1

Worker

A running process that hosts connectors and their tasks. Several workers together form a Connect cluster.

Part 2

Connector

The configuration-level object that decides how work is split into tasks.

Part 3

Task

The unit that actually moves data. A connector may run many tasks in parallel across workers.

Part 4

Converter and Transform

Converters change data formats, and transforms make small edits to each record on its way through.

Part 5

Offsets Storage

Internal Kafka topics where Connect remembers how far a source connector has read.

Part 6

Distributed Mode

Workers coordinate through Kafka itself, share tasks, and take over from failed workers. Standalone mode is a single process for testing.

flowchart LR
  E1["External system A"] --> S["Source task"]
  S --> K["Kafka topics"]
  K --> D["Sink task"]
  D --> E2["External system B"]
  W["Connect worker hosts the tasks"] -.-> S
  W -.-> D
        
FIG 1 — Source tasks feed Kafka, and sink tasks drain it, both hosted by workers
Simple Analogy

Kafka Connect is like a shipping port. Source connectors are the cranes unloading goods from outside ships onto the dock, which is Kafka. Sink connectors are the cranes loading goods from the dock onto outgoing ships. Workers are the crews, and tasks are the individual cranes.

3How Connect Remembers Its Progress

Every duplicate and every loss traces back to how progress is recorded. This is the key to the whole topic.

A task that moves data forever needs a bookmark, so that after a restart it can continue instead of starting over or skipping ahead. Source and sink connectors keep their bookmarks in completely different places, and this difference explains why their exactly-once stories are so different.

Source Connector Bookmarks

A source connector reads from a system that is not Kafka, so Kafka has no idea what position it has reached. Therefore each source record carries two small maps. The source partition describes which slice of the outside system the record came from, such as a table or a file name. The source offset describes the position inside that slice, such as a row number or a log position. The Connect framework periodically saves the latest source offsets into an internal Kafka topic. After a restart, the connector asks for its saved offsets and resumes from there.

Sink Connector Bookmarks

A sink connector reads from Kafka topics, so its bookmark is an ordinary consumer group offset. The framework commits these offsets to Kafka after the sink task has handed the records to the outside system. After a restart, the task continues from the last committed offset.

AspectSource ConnectorSink Connector
Reads fromAn external systemKafka topics
Writes toKafka topicsAn external system
Bookmark typeSource offsets, defined by the connectorConsumer group offsets
Bookmark stored inConnect’s internal offsets topicKafka’s consumer offsets storage
Who controls the positionThe connector author’s chosen formatKafka’s offset numbers
Exactly-once, by framework alonePossible with recent supportNot by itself, needs help from the destination
Simple Analogy

A source connector is a reader copying pages of an outside book into a Kafka notebook, and keeping a sticky note saying which outside page it reached. A sink connector is a reader copying pages from the Kafka notebook into an outside book, with the sticky note kept in the Kafka notebook. The trouble is that the note and the copying happen in two separate steps.

i
Key Idea

Whenever data movement and bookmark saving are two separate steps, a crash between them creates either a duplicate or a loss. Exactly-once is essentially the art of turning those two steps into one indivisible step.

4Where Duplicates and Gaps Come From

The failure windows are small and precise. Knowing them turns a mysterious problem into a solvable one.

The Classic Source Connector Window

Without special support, a source task produces records to Kafka, and later, on a timer, the framework saves the source offsets. Suppose the task has produced records for positions one to five hundred, but has saved offsets only up to four hundred. If the worker crashes now, the restart resumes at four hundred. The records from four hundred one to five hundred are produced a second time. Nothing was lost, but duplicates now sit in the topic.

1

The task reads a batch from the outside system

It builds records, each labeled with a source partition and source offset.

2

The framework sends the records to Kafka

The producer writes them to the topic partitions, and Kafka stores them permanently.

3

A crash happens before offsets are saved

The records are in Kafka, but the bookmark still points to an older position.

4

The restarted task resumes from the old bookmark

It reads the same outside data again and produces the same records a second time.

The Classic Sink Connector Window

A sink task receives records from Kafka, writes them to the outside system, and afterwards commits the consumer offsets. If it crashes after the write but before the offset commit, the restart resumes at the older offset and writes the same records again. If the outside system is not protected against repeats, duplicates appear there.

The Opposite Failure: Loss

Loss happens when the bookmark moves ahead of the actual work. For example, a sink that commits offsets first and writes afterwards can crash in between, and the restart skips records that were never written. That is why well-designed frameworks always move the bookmark after the work, accepting duplicates rather than loss. This is at-least-once behavior, and it is the default posture of Connect.

Crash TimingSource ResultSink Result
After work, before bookmarkDuplicate records in KafkaDuplicate rows in the destination
After bookmark, before workSkipped source data, if the order is wrongSkipped records, if the order is wrong
After bothCorrectCorrect
Retry of a failed writeProducer retry may duplicate without protectionRepeated insert unless the write is idempotent
!
Common Mistake

Teams often say their pipeline is exactly-once because they have never observed a duplicate. Duplicates appear only under crashes, restarts, rebalances, and network problems, which are rare in tests but certain in production over time.

5The Kafka Building Blocks Behind Exactly-Once

Connect does not invent exactly-once. It borrows tools that Kafka already provides for producers and consumers.

Understanding these tools first makes the Connect features easy to follow. There are four building blocks, each solving a different part of the puzzle.

Block 1

Idempotent Producer

Kafka gives each producer an identity and numbers its messages per partition, so the broker can spot and discard a retried duplicate.

Block 2

Transactions

A producer can write to several partitions and topics as one unit, which becomes visible together or not at all.

Block 3

Read-Committed Consumers

Consumers can be set to see only data from committed transactions, hiding data from aborted or unfinished ones.

Block 4

Fencing With Epochs

Each transactional producer has an epoch number. A newer instance with the same identity locks out any older instance.

Idempotence: Solving Retry Duplicates

When a producer sends a message and does not get a reply, it retries. The first message may have actually arrived, so the retry would create a duplicate. With idempotence turned on, the producer includes a sequence number, and the broker accepts each number only once per partition. A repeated message is quietly recognized and ignored. This covers duplicates from retries within a single producer session, but not duplicates created when a whole application restarts and reproduces old data.

Transactions: Solving Multi-Write Atomicity

A transaction lets a producer say begin, write many messages, possibly to many partitions, and then commit or abort. If it commits, all writes become visible together. If it aborts, none of them do. The transaction is tracked by a transactional identifier, which the producer chooses and reuses across restarts. Kafka writes a small control record called a marker into every involved partition to record the outcome.

Simple Analogy

A transaction is like writing several cheques and putting them in a sealed envelope. Nobody opens the envelope until you say it is final. If you change your mind, you shred the envelope, and the recipients never see a single cheque.

Read-Committed: Seeing Only Final Data

Consumers have an isolation setting. In the default mode, they read everything, including messages from transactions that are still open or later aborted. In the read-committed mode, they receive only messages from committed transactions, and they wait at a boundary called the last stable offset until open transactions finish. Without read-committed consumers, the benefit of transactions is invisible to readers.

Fencing: Solving the Zombie Problem

Suppose a worker becomes unresponsive, so the cluster starts a replacement task. Later the old task wakes up, unaware it has been replaced, and starts writing again. This is a zombie. Because the replacement has registered with the same transactional identifier and a higher epoch, the broker rejects every request from the old producer. The zombie is fenced out and cannot corrupt the data.

i
Putting It Together

Idempotence handles retries, transactions bundle records with bookmarks, read-committed consumers see only final results, and fencing stops old instances. Exactly-once in Connect is the coordinated use of all four.

6Exactly-Once for Source Connectors: The Core Idea

The big insight is to write records and bookmarks in the same Kafka transaction.

Exactly-once support for source connectors arrived in Kafka version 3.3. The insight behind it is elegant. Both the produced records and the source offsets ultimately live in Kafka topics. If they are written in one transaction, then they are committed together or aborted together. A crash can no longer leave records written but the bookmark stale, because both are in the same sealed envelope.

sequenceDiagram
  participant T as Source task
  participant P as Transactional producer
  participant D as Data topic
  participant O as Offsets topic
  T->>P: Begin transaction
  T->>P: Send batch of records
  P->>D: Write records
  P->>O: Write source offsets
  T->>P: Commit transaction
  P-->>D: Records become visible
  P-->>O: Bookmark becomes durable
        
FIG 2 — Records and bookmarks share one transaction
1

The task polls the connector for records

The connector returns a batch, each record labeled with the source offset it represents.

2

A transaction begins on the task’s own producer

The framework uses a transactional identifier unique to that task, so fencing is possible later.

3

Records go to data topics, offsets go to the offsets topic

Both writes happen inside the open transaction, and nothing is final yet.

4

The transaction commits at a chosen boundary

On commit, records and their matching bookmarks become final together.

5

After a crash, the task resumes from the last committed bookmark

Any records from an unfinished transaction are aborted and hidden from read-committed consumers, so the replay creates no visible duplicates.

“The bookmark and the data travel in the same envelope, so they can never disagree.”
!
Important Limit

This guarantee covers writing into Kafka. It assumes the outside source can be re-read from a saved position and produce the same data again. If the source cannot replay from a position, or returns different data on a second read, exactly-once cannot be created by Kafka alone.

A Worked Example With Simple Numbers

Let us follow an invented source connector that reads rows from a table using an ever-increasing row identifier as its source offset. The task starts with a committed bookmark of one thousand. It polls and receives rows one thousand one to one thousand fifty. It begins a transaction, writes those fifty records to the data topic, and writes the new bookmark, one thousand fifty, to the offsets topic. Then it commits.

Now suppose the worker crashes at three different moments in the next batch. In the first case, the crash comes after the task has written twenty records of the next batch but before it commits. The transaction never completes, so the twenty records are aborted, and the bookmark still says one thousand fifty. The restarted task reads rows one thousand fifty-one onward again and produces them in a new transaction. A read-committed consumer sees each row once.

In the second case, the crash comes right after the commit is requested. Kafka’s transaction coordinator either had recorded the commit decision, in which case the batch and its bookmark are both final, or it had not, in which case both are aborted. There is no third possibility where only one of them is final. In the third case, the crash comes just after a successful commit, so the restart begins from the new bookmark, and nothing is repeated or skipped.

50
Records per transaction in the example
1
Bookmark written per commit
0
Visible duplicates in all three crash cases

7The Configuration Surface, Explained

Exactly-once is opt-in, and it is controlled at two levels: the whole worker cluster and each individual connector.

Worker-Level Setting

Every worker in the Connect cluster has a setting that controls whether exactly-once support for sources is active. It has three values. Disabled means the feature is off, which is the default. Preparing means the worker is getting ready for the feature, and is used during a rolling upgrade. Enabled means the worker fully supports exactly-once source tasks. The feature requires distributed mode, because it depends on the cluster’s coordination and fencing abilities.

Connector-Level Settings

Setting 1

Exactly-Once Support Level

Each connector can request exactly-once or require it. If it is required and the connector cannot provide it, creation fails, which prevents silent downgrades.

Setting 2

Transaction Boundary

Decides when a transaction commits: after every poll, on a time interval, or whenever the connector says so.

Setting 3

Boundary Interval

The time limit used when the boundary mode is interval-based, trading latency against efficiency.

Setting 4

Offsets Storage Topic

A connector may use its own dedicated offsets topic, which can also hold the offsets in the same cluster as its data.

LevelPurposeTypical Choice
Worker modeTurns the capability on for the clusterEnabled after a staged rollout
Connector support levelDeclares how strict the connector isRequired for critical flows
Transaction boundaryControls commit frequencyPer poll for simplicity, interval for larger batches
Consumer isolationMakes readers see only committed dataRead-committed on all downstream consumers
i
Design Tip

Use the strict required level for connectors where duplicates would be costly. It converts a silent weakness into a loud error at creation time, when it is cheap to fix.

Before creating an exactly-once connector, you can also ask the framework to validate its configuration. The validation step reports whether the connector supports exactly-once, whether it can define its own boundaries, and whether the settings are consistent. It is a good habit to validate first and create second.

8Transaction Boundaries: A Deeper Look

A transaction boundary is the moment of commit, and choosing it shapes latency, throughput, and cost.

A boundary decides how many records share one transaction. Small transactions commit often, so consumers see data quickly, but each commit has a fixed cost. Large transactions amortize that cost, but readers must wait longer before data becomes visible, and a failure throws away more in-flight work.

Poll Boundary

Each batch returned by the connector’s poll becomes its own transaction. It is the simplest mode and gives the lowest delay, but when polls return tiny batches, the overhead of many small transactions can be significant.

Interval Boundary

Records accumulate in one open transaction, which commits after a configured time. This suits high-volume sources because many records share the cost of a commit, but data is invisible to read-committed consumers until the interval ends.

Connector-Defined Boundary

The connector itself signals where transactions should start and end, aligning them with natural units in the source, such as a database transaction or a file. This gives the most meaningful grouping, but only some connectors support it.

ModeVisibility DelayOverheadBest For
PollShortHigher when polls are smallLow-latency streams and simple setups
IntervalUp to the interval lengthLower per recordHigh-volume feeds that tolerate delay
Connector-definedDepends on source unitsDepends on unit sizeSources with natural atomic units
Simple Analogy

Consider a courier. Delivering each parcel separately is fast but expensive. Waiting an hour to fill the van is cheaper but slower. Delivering all the parcels from one shop together, whenever that shop finishes packing, matches the natural business unit.

!
Watch Out

A long transaction interval must stay well under the transaction timeout of the producer and broker. If a transaction stays open too long, the broker aborts it, and the task must retry. Choose intervals with a wide safety margin.

9Zombie Fencing and Task Reconfiguration

Distributed systems replace workers often, so protection against old, forgotten tasks is essential.

Connect clusters are dynamic. Workers join and leave, tasks are reassigned, and connectors may be reconfigured to run more or fewer tasks. Every one of these events risks leaving a stale task alive somewhere, writing to Kafka based on an outdated assignment.

To prevent this, exactly-once source tasks use transactional identifiers that are derived from the cluster, the connector, and the task number. Before a new generation of tasks starts, the framework initializes producers with those same identifiers, which raises their epochs and immediately blocks any older producer using the same identifier. A zombie’s next write is rejected, and it learns it was fenced.

sequenceDiagram
  participant L as Leader worker
  participant Old as Old task
  participant B as Kafka broker
  participant New as New task
  Old->>B: Writes inside transaction
  Note over Old: Worker stalls
  L->>B: Fence old producers with same identifiers
  L->>New: Start new task generation
  New->>B: Begin transaction with higher epoch
  Old->>B: Wakes up and tries to write
  B-->>Old: Rejected as fenced
        
FIG 3 — A fenced zombie cannot write after its replacement starts

When the Number of Tasks Changes

If a connector is reconfigured to use fewer tasks, the identifiers of the removed tasks would never be reused, and old instances of them might still be alive. The framework therefore fences those identifiers too, before allowing the new configuration to run. This is one reason why exactly-once source connectors have a somewhat more careful startup than ordinary ones.

Permissions Needed for Fencing

Fencing and transactions require the right permissions in secured clusters. The identity used by the workers must be allowed to write and describe the transactional identifiers it uses, and to write to the offsets topic. Missing permissions are a very common cause of failed exactly-once startups, and the error messages usually point to authorization.

i
Design Tip

Test the security setup for transactional identifiers in a staging environment that has the same access rules as production. Access problems are the most likely surprise on the first rollout.

10Turning It On Safely: A Rolling Upgrade

You should not flip a cluster-wide switch on a live system without a staged plan.

All workers in a Connect cluster must agree on how exactly-once tasks are fenced and started. If some workers are enabled while others are still disabled, the cluster could misbehave. The middle preparing state exists for exactly this reason, allowing a two-phase rollout with no downtime.

1

Confirm versions and permissions

Check that brokers and every worker run a version that supports the feature, and that the security rules for transactional identifiers exist.

2

Restart workers one by one in the preparing mode

Each worker is restarted with the preparing value. Nothing changes for connectors yet, but every worker now understands the new protocol.

3

Verify the whole cluster is prepared

Make sure every worker has been restarted and is healthy before going further.

4

Restart workers one by one in the enabled mode

Now the cluster fully supports exactly-once source tasks.

5

Reconfigure chosen connectors to require exactly-once

Update connectors one at a time, validate, and watch behavior before touching the next.

Rolling Back

Going back is also a staged process, moving from enabled to preparing and then to disabled, again worker by worker. Before rolling back, connectors that still require exactly-once should be reconfigured, or they will fail. Because downgrading affects duplicates, plan it with the same care as the upgrade.

!
Common Mistake

Skipping the preparing step and restarting workers straight into the enabled mode can leave a mixed cluster that behaves unpredictably during the rollout. The extra restart pass is the price of a safe transition.

11What Connector Authors Must Provide

The framework supplies the transactions, but the connector must be worthy of them.

A source connector cannot be exactly-once just because the framework is. The connector has to make certain promises, and the framework asks the connector to state them explicitly. This is why a connector may or may not support the feature, and why checking its documentation matters.

Duty 1

Declare Support

The connector must say whether it supports exactly-once. Connectors that never made the declaration are treated as unsupported.

Duty 2

Stable Source Offsets

Every record must carry a source offset that uniquely and precisely identifies a position in the source.

Duty 3

Repeatable Reads

Reading again from a saved offset must return the same records in the same order as the first time.

Duty 4

Correct Resume Logic

On startup, the connector must read its saved offsets and continue exactly after them, not before or beyond.

Duty 5

Optional Boundaries

Connectors that can mark natural transaction units can advertise that ability, enabling the connector-defined boundary mode.

ANTI-PATTERN-01 Avoid
Problem

A connector reads from a source using a query that returns rows in no fixed order, and labels each record with an offset based on how many rows it has read so far.

Why It’s Harmful

After a restart, the same query may return rows in a different order. The saved count then points to different rows, so some are skipped and others repeated, even with transactions in place.

Correct Approach

Use a real, stable position from the source, such as an increasing key, a change log position, or a file name with a byte position, and always read in a deterministic order.

i
For Users of Existing Connectors

If you did not write the connector, read its documentation for statements about exactly-once. If it is not mentioned, assume it is not supported and test it before relying on it.

12The Consumer Side of the Story

Transactional writing is only half of the guarantee. Readers must cooperate.

When a source task writes in transactions, records from open or aborted transactions physically exist in the topic. They are hidden only from consumers that ask for committed data. A consumer using the default setting will happily read records from aborted transactions, and will see the very duplicates that the transaction was meant to hide.

ANTI-PATTERN-02 Avoid
Problem

A team enables exactly-once on a source connector but leaves the downstream consumers on their default reading mode.

Why It’s Harmful

Those consumers see records from aborted attempts, so duplicates still appear downstream. The team has paid the cost of transactions and received none of the benefit.

Correct Approach

Set every consumer of transactional topics to read only committed data, and add a check to onboarding steps for new consumers.

Side Effects of Read-Committed Reading

Delayed Visibility

Consumers cannot read past the oldest open transaction. If the source uses long intervals, downstream data arrives in bursts at commit time.

Blocked Partitions

A transaction that stays open, because a task is stuck, holds back every read-committed consumer of that partition until it is aborted by timeout. One stuck producer can stall many readers.

Offset Gaps

Markers and aborted records use up offsets, so offsets in the topic are not perfectly consecutive. Applications must not assume otherwise.

i
Good to Know

Kafka aborts transactions that exceed the timeout, which clears blocked partitions. Still, a long timeout means a long stall, so watch the age of open transactions and alert on stuck ones.

13Sink Connectors: A Different Kind of Problem

Sinks write to systems that Kafka cannot control, so the framework alone cannot promise exactly-once.

The transaction trick works for sources because both records and bookmarks are written into Kafka. In a sink, the records go to an outside system, such as a database, a search index, or object storage. Kafka’s transactions cannot include a write to an arbitrary outside system, so the sink’s two steps, writing to the destination and committing consumer offsets, cannot be made one atomic step by the framework.

Instead, exactly-once in a sink is achieved by making the destination side safe. There are three well-known strategies. Which one you can use depends on what the destination system offers.

flowchart TD
  K["Kafka topic records"] --> S["Sink task"]
  S --> A["Strategy A: idempotent writes by key"]
  S --> B["Strategy B: store offsets with data atomically"]
  S --> C["Strategy C: deterministic output names"]
  A --> X["Destination system"]
  B --> X
  C --> X
        
FIG 4 — Three ways to make a sink effectively exactly-once

Strategy A: Idempotent Writes

Design the write so that repeating it has no extra effect. For example, use an upsert keyed by a natural or event identifier, so a repeated record overwrites the same row with the same values. This is the simplest and most common approach when the destination supports it.

Strategy B: Store Offsets in the Destination

Write the data and the Kafka offsets into the destination in one local transaction. On startup, the task reads the stored offsets from the destination and tells the framework to resume from there, ignoring the consumer group offsets. The destination is now the single source of truth for progress.

Strategy C: Deterministic Output

For file or object stores, name each output file from the topic, partition, and starting offset, so a retried write recreates the same file and overwrites it. Because the content for a given offset range is identical each time, repeats are harmless.

Simple Analogy

Think of a clerk filing paper forms in a cabinet. Idempotent writing means each form has a unique number, and filing the same form twice puts it in the same slot. Storing offsets means the clerk writes the last form number on a card kept inside the cabinet. Deterministic output means each folder is named by the range of forms it holds.

StrategyNeeds From DestinationStrengthWeakness
Idempotent writesUpserts or unique keysSimple and robustRequires a natural key in the data
Offsets stored with dataLocal transactionsPrecise progress controlNeeds custom connector logic
Deterministic outputOverwritable files or objectsFits storage sinks wellDepends on stable batch boundaries

Choosing the Right Strategy

The choice depends on the destination. Start by asking whether the destination can recognize a repeated record on its own. Relational databases, key-value stores, and search indexes usually can, through primary keys or document identifiers, so idempotent writes are the natural first choice. It needs no special connector logic, and it survives every kind of replay, including ones caused by operators who reset offsets by hand.

If the destination supports local transactions but has no natural key in the data, storing offsets alongside the data becomes attractive. The price is custom logic, because the connector must read stored positions at startup and override the framework’s own starting point. If the destination is a file or object store, deterministic naming is usually the cleanest answer, provided the connector always cuts files at repeatable boundaries. A connector that cuts files by wall-clock time, for example, may produce differently sized files on a replay and leave overlapping content behind.

Destination TypeNatural ChoiceWatch Out For
Relational databaseUpsert by primary keyTables without unique keys
Search indexDocument identifier from the eventAuto-generated identifiers on each write
Object or file storageDeterministic names from topic, partition, offset rangeTime-based file cutting
Message queue or APIIdempotency key in each requestEndpoints that ignore repeat keys
!
Common Misconception

Some assume that transactional sources imply transactional sinks. They do not. Enabling exactly-once for sources says nothing about what a sink does, and a sink connector’s documentation must be checked separately.

14Sink Task Behavior: Commits, Rebalances, and Errors

Small details in how sink tasks commit progress decide how many duplicates you will see.

The Sink Task Cycle

A sink task repeatedly receives a batch of records from the framework, writes them to the destination, and periodically reports how far it can safely commit. The framework then commits consumer offsets up to that point. The frequency of commits is controlled by a timer, so between commits, a crash replays everything since the last commit. Longer intervals mean better performance and more replay after a crash.

Sink tasks may also hold records in a buffer, for example to build larger files. A well-behaved task must report only offsets that have really been written, not offsets it has merely received. If it reported buffered records as done, a crash would lose them. This is a frequent source of subtle loss in custom connectors.

Rebalances

When the set of tasks changes, partitions are reassigned, and each sink task is told which partitions it lost and gained. A careful connector flushes and commits work for lost partitions before releasing them, and starts from the right position for gained partitions. Improper handling here creates duplicates or gaps around every rebalance, which is why cooperative rebalancing and careful hand-off logic matter.

Errors, Retries, and Dead-Letter Queues

Connect can be told to tolerate errors, retry failed operations, and send bad records to a dead-letter topic. These settings interact with exactness. If the framework skips or diverts a record, the sink no longer contains every input record, so completeness is deliberately given up in exchange for progress. A retry may repeat a write, which is harmless only with idempotent destinations.

Setting 1

Commit Interval

Controls how often offsets are committed. Shorter intervals reduce replay, and longer ones reduce overhead.

Setting 2

Error Tolerance

Chooses between stopping the task on error and skipping bad records, which changes completeness guarantees.

Setting 3

Retry Behavior

Determines how many times and how long failed operations are retried, which can repeat writes.

Setting 4

Dead-Letter Topic

Captures records that could not be processed, so they can be inspected and replayed later.

i
Design Tip

Decide per pipeline whether completeness or continuity matters more. For financial data, stop on errors and investigate. For telemetry, skipping and diverting bad records may be the healthier choice.

15End-to-End Pipelines: The Weakest Link Rule

A pipeline is only as exact as its least careful stage.

Real pipelines chain several stages. Data may flow from a database through a source connector into Kafka, through a stream processing application, into another topic, and finally through a sink connector into a warehouse. Each stage has its own guarantee, and the guarantee of the whole is the weakest of them.

flowchart LR
  DB["Source database"] --> SC["Source connector: transactional"]
  SC --> T1["Kafka topic"]
  T1 --> SP["Stream processor: transactional"]
  SP --> T2["Kafka topic"]
  T2 --> SK["Sink connector: needs idempotent destination"]
  SK --> DW["Data warehouse"]
        
FIG 5 — Each stage adds its own guarantee, and the chain is only as strong as its weakest stage
StageHow It Can Be Exactly-OnceTypical Risk
External source to KafkaTransactional source connector with replayable sourceSource cannot replay consistently
Inside Kafka processingTransactional processing that ties output to input offsetsReaders not using committed reading
Kafka to external sinkIdempotent, offset-aware, or deterministic writesDestination cannot deduplicate
Whole pipelineAll three combined and testedOne weak stage undoes the rest
Simple Analogy

A relay race team can only be as fast as its slowest runner. Three brilliant sprinters and one who stops to tie a shoe still lose. Exactly-once across a pipeline works the same way, because one stage that repeats work can spoil the whole chain.

!
Watch Out

Downstream systems that receive data through an unprotected side channel, such as a custom script that copies topic data into a database, break the guarantee at the last step. Audit every exit from Kafka, not just the connectors.

16The Cost of Exactly-Once

Stronger guarantees are not free, and you should know the price before paying it.

Transactions add work. Every commit involves extra messages between the producer and the broker, and control records are written into every partition touched. Records are invisible to careful readers until commit, and tasks must fence and coordinate on startup. For most workloads the overhead is modest, but it is real, and it grows when transactions are tiny.

Advantages of Exactly-Once

  • No visible duplicates from source connector crashes and restarts
  • No need for downstream deduplication logic on those topics
  • Safer handling of financial and audit data
  • Clear, verifiable behavior after failures
  • Loud failures when a connector cannot meet the requirement

Disadvantages / Trade-offs

  • Higher latency, since data is visible only after commit
  • Extra broker work and control records
  • Requires distributed mode and compatible versions
  • Only supported by connectors that opt in
  • More complex security setup and troubleshooting
  • Long transactions can stall read-committed consumers

Estimating the Overhead for Your Workload

The cost of a transaction is roughly fixed per commit, so the number of commits per second matters more than the number of records. If a connector polls ten times a second and uses the per-poll boundary, that is ten transactions a second per task. With fifty tasks, the cluster handles five hundred commits a second, each touching the data topics and the offsets topic. That is usually fine, but it is very different from a single task using a one-second interval.

A practical way to estimate is to run a short load test with the boundary you plan to use, then compare throughput and latency with a non-transactional run. Check three numbers: the delay between a record being produced and being visible to committed readers, the broker’s request rates, and the size of the internal transaction state. If the delay is acceptable and the broker load is comfortable, keep the setting. If not, lengthen the boundary a little and test again.

When Exactly-Once Is Not Worth It

Not every pipeline needs the strongest promise. Metrics, logs, and clickstreams that are aggregated or sampled tolerate an occasional duplicate, and idempotent downstream logic can cheaply remove them. Paying the transaction cost for data that no one would miss is wasted effort. A helpful question is: what is the business cost of one duplicate, and one missing record?

Latency
Visibility waits for commit
Overhead
Extra commits and markers
Certainty
Clear behavior after failure

17Failure Scenarios Walked Through

The best way to trust a guarantee is to watch it survive the failures you fear.

Worker Crash in the Middle of a Transaction

A source task has produced records inside an open transaction when its worker dies. The transaction never commits, and it is eventually aborted. Read-committed consumers never see those records. Another worker restarts the task from the last committed bookmark, and reproduces the data in a fresh transaction.

Broker Failure During Commit

If the broker leading the transaction coordinator fails, leadership moves to another broker, which recovers the transaction state from its log. The producer retries the commit, and the outcome is decided exactly once. The task sees a brief delay, not duplicates.

Network Partition Isolating a Worker

The isolated worker keeps running and cannot tell it has been replaced. The cluster starts a replacement task and fences the old identifiers. When the network heals, the old task’s writes are rejected, and it stops.

Rebalance of Connect Tasks

A worker joins, and tasks are moved. Exactly-once source tasks are fenced and restarted from committed bookmarks. In sinks, tasks flush pending work for released partitions, and new owners resume from committed positions.

Offsets Topic Unavailable

Because the bookmark is part of the transaction, if the offsets topic cannot be written, the transaction cannot commit, and no records become visible. The pipeline stalls rather than producing data it cannot bookmark, which is the safe choice.

Transaction Timeout Exceeded

A transaction stays open too long, perhaps because of a slow source. The broker aborts it, the producer is fenced by the timeout, and the task must restart the batch. Records are never half committed, but throughput dips.

i
Design Tip

Turn these scenarios into rehearsals in a test environment. Kill workers at random moments, pause brokers, and cut networks, then compare the source count with the visible topic count. A real test is worth more than any claim in documentation.

18Monitoring, Logging, and Security

Exactly-once is a property you must keep verifying, not a setting you set once.

Metrics to Watch

Signal 1

Task Status and Restarts

Frequent restarts suggest fencing, timeouts, or permission problems that need attention.

Signal 2

Transaction Age

Old open transactions hold back readers. Alert when they approach the timeout.

Signal 3

Source Lag

How far behind the source the connector is, which reveals whether commit overhead is slowing the flow.

Signal 4

Consumer Lag on Committed Reads

Growing lag with bursty progress often means transactions are too long.

Signal 5

Sink Commit Age

Time since the last offset commit, which tells you how much replay a crash would cause.

Signal 6

Error and Dead-Letter Counts

Any growth means records are being diverted, changing the completeness of your data.

Reconciliation Checks

Independent counts are the ultimate test. Periodically compare the number of source records for a time window with the number of visible records in Kafka, and again with the number in the destination. Small differences during in-flight windows are normal, but persistent gaps or surpluses reveal a broken guarantee, often long before customers notice.

Security

Exactly-once adds security requirements. The workers need permission to use transactional identifiers and to write to the offsets topics, and those topics contain positions that reveal information about the source. Protect them like any sensitive topic, limit who can change them, and audit changes. An attacker who could rewrite offsets could force replays or skips, so treat offsets topics as control data.

!
Watch Out

Do not give broad administrative rights to the Connect identity just to make transactions work. Grant the specific permissions needed and review them regularly.

19Design Patterns That Work

Teams that run reliable pipelines share a handful of habits.

Pattern: Identify Every Event With a Stable Key

Give every business event a unique identifier at its origin. It makes downstream deduplication and idempotent writes possible, even when a stage cannot offer transactions.

Pattern: Defense in Depth

Use transactions where available and still design destinations to tolerate repeats. If one layer fails, another catches it.

Pattern: Require, Do Not Request

For critical flows, require exactly-once so that a connector that cannot deliver it fails loudly instead of quietly downgrading.

Pattern: Read-Committed by Default

Make read-committed reading the standard for every consumer template in your organization, so no team forgets it.

Pattern: Reconcile Continuously

Run automated count and checksum comparisons between source, Kafka, and destination, and page someone when they drift.

ANTI-PATTERN-03 Avoid
Problem

A sink writes plain inserts into a database, with no unique key on the target table, and the team relies on the pipeline being exactly-once.

Why It’s Harmful

Any replay after a crash inserts the same rows again, and nothing in the database can recognize them as duplicates. The problem is discovered later, in reports that do not add up.

Correct Approach

Add a unique key from the event identifier, and change the write to an upsert, so replays are harmless.

ANTI-PATTERN-04 Avoid
Problem

The transaction interval is set to several minutes to save overhead, while the transaction timeout is left near its default.

Why It’s Harmful

Transactions get aborted before they commit, tasks restart repeatedly, and progress stalls or crawls. Readers see nothing for long periods.

Correct Approach

Keep the interval far below the timeout, measure real commit times, and prefer shorter transactions unless you have a proven need for longer ones.

20Best Practices and Common Mistakes

A short checklist prevents most painful surprises.

Best Practices

  • Confirm each connector’s support for exactly-once before relying on it
  • Roll out worker changes in the staged preparing then enabled order
  • Set downstream consumers to committed reading
  • Keep transaction intervals well below timeouts
  • Use stable, deterministic offsets in custom connectors
  • Make sink writes idempotent whenever the destination allows
  • Reconcile counts across stages on a schedule
  • Rehearse failures in a test environment

Common Mistakes

  • Assuming sinks are exactly-once because sources are
  • Leaving consumers on default reading mode
  • Skipping the preparing stage during rollout
  • Using non-repeatable source queries with row counts as offsets
  • Missing permissions for transactional identifiers
  • Reporting buffered records as committed in sink tasks
  • Ignoring dead-letter growth
  • Never testing crash behavior
i
A Simple Rule of Thumb

For every stage in your pipeline, be able to answer one question in one sentence: what makes a repeated attempt harmless here? If you cannot, that stage is your weak link.

21Real-World and Industry Context

Duplicate data hurts in different ways in different industries.

For years, the standard advice was to accept at-least-once behavior in Connect and to deduplicate downstream. That advice still holds for many pipelines. Native support for exactly-once source connectors, added in Kafka 3.3, gave teams a stronger option where duplicates are expensive or hard to remove later. Connector projects have gradually adopted the feature, and the level of support varies by connector and version, so always confirm with the connector’s own documentation.

Change Data Capture From Databases

Capturing every change from an operational database and delivering it into Kafka is a classic use. A duplicate change event can corrupt a replica or a derived table, so teams value transactional writing and stable log positions as offsets.

Payments and Ledgers

A duplicated payment record is a financial error. Teams combine transactional sources, read-committed processing, and upserts keyed by transaction identifier, and reconcile totals daily.

Data Lake and Warehouse Loading

Sinks that write files often name them by topic, partition, and offset range, so retries overwrite rather than duplicate. Warehouse loads use merge statements keyed by event identifiers.

Telemetry and Logs

High-volume, low-value data often chooses at-least-once with cheap downstream deduplication, because the cost of transactions outweighs the value of perfection.

22Proving the Guarantee: How to Test It

A guarantee that has never been attacked is only a hope.

Testing exactly-once means creating the very failures that would break it, and then checking the result with independent counts. It does not require complex tools. It requires a controlled source with a known number of records, a running pipeline, and a plan for causing trouble.

1

Prepare a source with known content

Load a fixed set of numbered records, so any missing or repeated number is easy to detect.

2

Run the pipeline while causing failures

Kill workers at random times, restart brokers, pause networks, and trigger rebalances while data flows.

3

Read the results with committed reading

Consume the topic with a read-committed consumer and record every record number seen.

4

Compare with the source

Every number must appear once. Any missing or repeated number is a failure that needs investigation.

5

Repeat with the destination

For sinks, run the same test and compare the destination’s contents with the input, checking for both duplicates and gaps.

i
Design Tip

Also run the test with a plain read-everything consumer. Seeing duplicates from aborted attempts in that run, and none in the committed run, is direct evidence that transactions are doing their job.

23Frequently Asked Questions

Q1Is Kafka Connect exactly-once by default?

No. The default behavior is at-least-once. Exactly-once for source connectors must be enabled at the worker and connector level, and the connector itself must support it.

Q2Does exactly-once work for sink connectors?

Not through the framework alone. Sinks write to outside systems, so exactness comes from idempotent writes, from storing offsets together with data, or from deterministic output naming.

Q3Why must consumers use read-committed reading?

Because records from aborted transactions still exist in the topic. Only committed reading hides them. Without it, readers see the duplicates that transactions were designed to remove.

Q4Can I use exactly-once in standalone mode?

The feature is designed for distributed mode, because it relies on cluster coordination and fencing between workers. Standalone mode is meant for testing and simple cases.

Q5What happens if a connector does not support exactly-once but I require it?

Creating the connector fails with a validation error. That is the intended behavior, since it prevents a silent downgrade to weaker guarantees.

Q6What is a zombie task?

An old task instance that was replaced but is still running and does not know it. Fencing with transactional identifiers and epochs makes the broker reject its writes.

Q7How do I choose a transaction boundary?

Use per-poll transactions for simplicity and low delay, interval-based ones for high volume sources that tolerate delay, and connector-defined ones when the source has natural atomic units and the connector supports them.

Q8Why did my downstream consumers stop receiving new data?

With committed reading, consumers cannot pass an open transaction. A stuck or long transaction can hold back readers until it commits or times out. Check transaction age and task health.

Q9Do transactions slow my pipeline a lot?

Usually modestly. The cost is higher when transactions are tiny, and visibility is delayed until commit. Measure with your own workload and tune the boundary accordingly.

Q10Is exactly-once the same as no data loss?

Not exactly. It combines no loss with no visible duplicates, but only within its defined scope. Skipped records from error tolerance, or data that never reached Kafka, are outside that promise.

Q11Should I still deduplicate downstream?

For critical data, keeping idempotent downstream logic is wise as a second line of defense. It protects you from connector bugs, configuration mistakes, and stages that cannot offer transactions.

Q12What if the outside source returns different data on a second read?

Then exactly-once cannot be guaranteed for that source, because the replay is not the same data. The source must provide a stable, repeatable position for the guarantee to hold.

Q13How do I roll out the feature on a live cluster?

Verify versions and permissions, restart workers one by one into the preparing mode, verify, restart them again into the enabled mode, and then move connectors to the required level one at a time.

Q14How do I verify my pipeline is really exactly-once?

Load a source with numbered records, cause random failures during the run, read the results with committed reading, and confirm every number appears exactly once. Repeat with the destination for sinks.

Q15Can I change a running connector from at-least-once to exactly-once?

Yes, once the workers are enabled, by updating the connector’s configuration. Plan for a short restart of its tasks, validate first, and expect that records already written before the change may include earlier duplicates.

Q16Do transactions change how many partitions or topics I need?

No. Transactions work across any number of partitions and topics. They do add markers to each partition touched, so extremely wide transactions across many partitions cost more per commit.

24Summary and Key Takeaways

Kafka Connect moves data between Kafka and the outside world, and its default posture is at-least-once, because progress is saved after work is done. Duplicates appear when a crash falls between the work and the bookmark. Exactly-once removes that gap by making the two inseparable, using idempotent producers, transactions, committed reading, and fencing.

For source connectors, recent Kafka versions write records and source offsets in a single transaction, provided the cluster runs in distributed mode, the workers are enabled in a staged rollout, and the connector supports the feature and reads from a replayable source. For sink connectors, exactness has to be built on the destination side, through idempotent writes, offsets stored with the data, or deterministic outputs. Across a whole pipeline, the weakest stage decides the outcome, so every stage must be examined and tested.

Key Takeaways

  • Default is at-least-once — Connect saves progress after work, so crashes can cause duplicates but not loss.
  • Sources use transactions — records and source offsets are committed together, so the bookmark and the data can never disagree.
  • Readers must cooperate — consumers need committed reading, or they will still see records from aborted attempts.
  • Fencing stops zombies — transactional identifiers and epochs make brokers reject writes from replaced tasks.
  • Sinks need destination help — use idempotent writes, offsets stored with data, or deterministic file names.
  • Roll out in stages — move workers through preparing to enabled, and require the feature on connectors one at a time.
  • Test the guarantee — cause failures on purpose, reconcile counts across stages, and trust evidence over documentation.