Kafka Connect Deep Dive
Kafka Connect is the data integration layer of the Kafka ecosystem. It bridges the gap between Kafka and every other system you run — databases, object stores, search engines, data warehouses, SaaS APIs — using a framework of pluggable, scalable, fault-tolerant connectors. It looks deceptively simple in the docs: deploy a JAR, POST a JSON config, data flows. Then you hit production, and the questions start. Why did the connector pause? What is a replication slot and why is my Postgres disk filling up? How do I guarantee exactly-once delivery for a source connector? Why did schema evolution break my consumers at 2 AM?
This guide covers all of it — architecture, CDC with Debezium, transforms, exactly-once semantics, Schema Registry, error handling, Kubernetes deployment with Strimzi, and monitoring. All configuration examples are tested against current versions: Debezium 3.5.0.Final (released March 2026), Strimzi 0.51, and Apache Kafka 4.1.
Architecture
What Kafka Connect Is
Kafka Connect is a JVM process (a worker) that runs connectors. Each connector is a plugin — a JAR on the worker’s classpath — that implements the Connect API. The framework handles everything else: distributed coordination, offset storage, task lifecycle, REST management, error handling, and schema conversion. You write (or download) the connector logic; Connect handles the operations.
There are two flavors of connector:
- Source connectors pull data from an external system and write records to Kafka topics.
- Sink connectors consume records from Kafka topics and write them to an external system.
Workers: Standalone vs Distributed
Standalone mode runs a single worker process. Offsets and configs are stored in local files. It exists for development and one-off migrations, not production. Never use it for anything that needs to survive a process restart reliably.
Distributed mode is what you run everywhere else. Workers form a cluster via a Kafka consumer group. The cluster state — connector configurations, task offsets, connector/task statuses — is stored in three compacted Kafka topics. Any worker in the group can serve the REST API, and requests are forwarded to the right worker transparently.
┌──────────────────────────────────────────────────────────────┐
│ Kafka Connect Cluster │
│ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │
│ │ │ │ │ │ │ │
│ │ Task A/0 │ │ Task A/1 │ │ Task A/2 │ │
│ │ Task B/0 │ │ │ │ Task B/1 │ │
│ │ │ │ │ │ │ │
│ │ REST :8083 │ │ REST :8083 │ │ REST :8083 │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │ │ │
│ └───────────────┴───────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Kafka Broker │ │
│ │ │ │
│ │ connect-configs │ ← connector configs │
│ │ connect-offsets │ ← source offsets │
│ │ connect-statuses │ ← connector/task state │
│ └────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Internal Topics
Three internal topics are created automatically on first startup. Their names are configured via:
|
|
Recommended settings for production:
| Topic | Partitions | Replication Factor | Cleanup Policy |
|---|---|---|---|
connect-configs |
1 | 3 | compact |
connect-offsets |
25 | 3 | compact |
connect-statuses |
5 | 3 | compact,delete |
The config topic has exactly one partition because it is ordered (serialized writes). The offset topic benefits from more partitions for throughput when running many connectors. Create these topics manually before starting workers if you need specific configurations — Connect will create them with its defaults if they don’t exist, and those defaults (replication factor 1) are wrong for production.
Connectors, Tasks, and Parallelism
A connector is a logical entity with a name and a configuration. It is responsible for determining how many tasks to create and what each task does. A task is the actual unit of work — a thread (or lightweight concurrency unit) doing the I/O.
tasks.max sets the ceiling on parallelism. The connector determines the actual task count based on what makes sense for the source. A Debezium connector for a single database will always use one task regardless of tasks.max, because CDC from a single transaction log is inherently sequential. A JDBC source connector polling 20 tables with tasks.max=5 will create 5 tasks covering 4 tables each.
Rebalancing
Workers use Kafka’s consumer group protocol to elect a leader and distribute work. When a worker joins, leaves, or fails, a rebalance happens. Since Kafka 2.3, Connect uses Incremental Cooperative Rebalancing (KIP-415), which avoids the stop-the-world full rebalance of the old eager protocol. In cooperative rebalancing, workers give up only the tasks that need to move rather than stopping everything.
During rebalance, the leader worker computes a new assignment and communicates it via the consumer group protocol. The REST API returns HTTP 409 (Conflict) on connector operations during an active rebalance.
REST API Forwarding
Every worker exposes a REST API on port 8083. You can POST to any worker and it will forward the request to the correct worker if needed. The rest.advertised.host.name and rest.advertised.port settings control how workers advertise themselves to each other. In Kubernetes, set rest.advertised.host.name to the pod’s IP (use the downward API) so internal forwarding uses pod IPs rather than hostnames that might not resolve correctly.
Worker Configuration
A minimal distributed worker connect-distributed.properties:
|
|
The REST API
Kafka Connect’s REST API is the primary operational interface. All operations are idempotent except connector creation (POST).
Creating a Connector
|
|
Checking Status
|
|
Response structure:
|
|
Updating Configuration
|
|
Lifecycle Operations
|
|
Connector Types and Popular Connectors
Source Connectors
Source connectors read from an external system and write events to Kafka. They implement SourceConnector and SourceTask. The task’s poll() method returns a list of SourceRecord objects each time it is called.
Popular source connectors:
| Connector | Class | Use Case |
|---|---|---|
| Debezium PostgreSQL | io.debezium.connector.postgresql.PostgresConnector |
CDC via pgoutput logical replication |
| Debezium MySQL | io.debezium.connector.mysql.MySqlConnector |
CDC via binary log |
| Debezium MongoDB | io.debezium.connector.mongodb.MongoDbConnector |
CDC via oplog/change streams |
| Debezium SQL Server | io.debezium.connector.sqlserver.SqlServerConnector |
CDC via SQL Server CDC feature |
| JDBC Source | io.confluent.connect.jdbc.JdbcSourceConnector |
Poll-based ingestion (no CDC) |
| MirrorMaker2 | (built-in) | Cross-cluster Kafka replication |
Sink Connectors
Sink connectors consume from Kafka topics and write to external systems. They implement SinkConnector and SinkTask. The task receives batches of SinkRecord objects via put().
Popular sink connectors:
| Connector | Class | Use Case |
|---|---|---|
| S3 Sink | io.confluent.connect.s3.S3SinkConnector |
Kafka → S3 data lake |
| JDBC Sink | io.confluent.connect.jdbc.JdbcSinkConnector |
Kafka → RDBMS |
| Elasticsearch/OpenSearch | io.confluent.connect.elasticsearch.ElasticsearchSinkConnector |
Kafka → search index |
| HTTP Sink | various | Kafka → REST API |
| Debezium JDBC Sink | io.debezium.connector.jdbc.JdbcSinkConnector |
Debezium-native sink, schema-aware |
| BigQuery | com.wepay.kafka.connect.bigquery.BigQuerySinkConnector |
Kafka → BigQuery |
| Snowflake | com.snowflake.kafka.connector.SnowflakeSinkConnector |
Kafka → Snowflake |
Complete Connector Configuration Examples
Debezium PostgreSQL (CDC Source)
|
|
JDBC Source Connector
|
|
S3 Sink Connector
|
|
Single Message Transforms (SMTs)
SMTs are lightweight, stateless transformations applied to each record as it passes through Connect. They run after the converter deserializes the bytes into a SourceRecord (for source connectors) or before the converter serializes it (for sink connectors).
SMTs are not a stream processing replacement — they cannot join streams, aggregate records, or maintain state. For anything beyond per-record filtering and field manipulation, use Kafka Streams or Flink.
Configuring Transforms
|
|
Transforms are applied in the order they are listed in the transforms property. Each transform has a name (you choose it), a type, and any type-specific properties prefixed with the name.
Built-In SMTs Reference
| SMT | Class | What It Does |
|---|---|---|
ExtractField |
...ExtractField$Key / $Value |
Pull a single field out of a struct, making it the new key/value |
ReplaceField |
...ReplaceField$Value |
Rename, include (whitelist), or exclude (blacklist) fields |
MaskField |
...MaskField$Value |
Replace field value with type-appropriate zero/null/empty |
Filter |
...Filter |
Drop records matching a predicate (use with Predicates) |
InsertField |
...InsertField$Value |
Add a field with a static value, partition, offset, or timestamp |
TimestampConverter |
...TimestampConverter$Value |
Convert between unix epoch ms, Date, Time, Timestamp, string |
ValueToKey |
...ValueToKey |
Copy fields from the record value into the key |
RegexRouter |
...RegexRouter |
Rewrite topic names with a regex replacement |
SetSchemaMetadata |
...SetSchemaMetadata$Value |
Override schema name or version |
Flatten |
...Flatten$Value |
Flatten nested structs to top-level with dot-notation field names |
Cast |
...Cast$Value |
Change field types (STRING → INT32, etc.) |
HoistField |
...HoistField$Value |
Wrap the entire value in a struct under a named field |
DropHeaders |
...DropHeaders |
Remove specific headers from the record |
For Debezium specifically, the ExtractNewRecordState SMT (bundled with Debezium) is essential — it unwraps the CDC envelope to expose just the after field as the record value.
Using Predicates with Filter
Predicates attach boolean logic to any SMT, making the transform conditional:
|
|
Other built-in predicates: HasHeaderKey, TopicNameMatches.
Writing a Custom SMT
A custom SMT is a JAR that implements org.apache.kafka.connect.transforms.Transformation<R>:
|
|
Deploy by placing the JAR in your plugin.path directory. Configure it like any other SMT:
|
|
Debezium in Depth
Debezium is the most important source connector ecosystem for Kafka. It implements Change Data Capture (CDC) by reading database transaction logs rather than polling tables. This gives you:
- Real-time row-level change events (insert, update, delete)
- Zero additional load on the database from polling
- Exact change ordering per table
- A complete history of changes, not just snapshots
As of Debezium 3.5.0.Final (March 2026), supported databases include:
| Database | Log Mechanism | Notes |
|---|---|---|
| PostgreSQL | Logical replication (pgoutput / decoderbufs) | pgoutput is native since PG 10; no extension needed |
| MySQL | Binary log (binlog) | Requires binlog_format=ROW |
| MariaDB | Binary log | Separate connector from MySQL since Debezium 2.5 |
| MongoDB | Change streams / oplog | Requires replica set or sharded cluster |
| SQL Server | SQL Server CDC feature | Requires CDC enabled per-table |
| Oracle | LogMiner | Requires supplemental logging; significant DBA setup |
| Db2 | ASN Agent / journal | z/OS and LUW |
| CockroachDB | Changefeeds | Added in 3.5 with snapshot support |
| Informix | CDC API |
The Debezium Envelope Format
Every Debezium event is a Kafka message with a structured envelope value. Understanding this format is essential before deploying Debezium anywhere.
|
|
op field values:
c— create (INSERT)u— update (UPDATE)d— delete (DELETE)r— read (snapshot row)t— truncate (table TRUNCATE, PostgreSQL only)m— message (Debezium heartbeat / logical decoding message)
before is null for inserts. after is null for deletes.
For deletes, Debezium also emits a tombstone record — a message with the same key but a null value — after the delete event. Tombstones signal Kafka’s log compaction to remove all prior messages for that key. You can suppress tombstones with tombstones.on.delete=false, but you should understand why they exist before doing so.
Snapshot Modes
The snapshot is what runs when the connector first starts (or when offset data is absent) to capture the existing state of the database before beginning to stream live changes.
| Mode | Behavior |
|---|---|
initial (default) |
Snapshot tables once, then stream changes. Skips snapshot if offsets already exist. |
initial_only |
Snapshot and stop. Does not stream changes. Useful for one-time migrations. |
schema_only |
Capture schema without row data, then stream changes. Use when you only care about events from now on. |
never |
No snapshot. Starts streaming from current log position. Risky — any rows that existed before the connector started will never appear. |
always |
Snapshot every time the connector starts, even if offsets exist. Expensive; rarely correct. |
when_needed |
Snapshot only when the existing offset is no longer valid (e.g. after a slot was dropped). |
no_data |
Capture schema only (alias for schema_only in some versions). |
For most production deployments, initial is correct. Use schema_only when you’re adding Debezium to a running system where downstream consumers will handle missing history.
PostgreSQL Deep Dive
PostgreSQL CDC uses logical replication, which streams WAL (Write-Ahead Log) events via a replication slot. The slot is a cursor into the WAL that PostgreSQL preserves until Debezium confirms it has consumed and committed up to a given LSN (Log Sequence Number).
Setup requirements:
|
|
Offset management:
Debezium stores the current LSN in Kafka Connect’s offset topic. Each record in the offset topic looks like:
|
|
The connector advances the confirmed LSN only after the Kafka producer successfully commits the batch of change events. This means the PostgreSQL replication slot holds the WAL from the last confirmed LSN forward.
Replication slot lag — the critical production issue:
If Debezium stops consuming (connector paused, Kafka unavailable, task crashed), the replication slot keeps retaining WAL. PostgreSQL cannot reclaim WAL space past the slot’s restart_lsn. Left unchecked, this fills your disk and causes a PostgreSQL outage.
Monitor slot lag continuously:
|
|
Alert when lag_size exceeds 5 GB. Set max_slot_wal_keep_size = 20GB (PostgreSQL 13+) to automatically invalidate the slot if it falls too far behind — this is a failsafe, not a solution. The real solution is keeping the connector running and monitoring its health.
Heartbeat tables:
On low-traffic databases, the connector’s LSN may not advance because no changes are happening in the monitored tables. The slot still holds WAL from other unrelated activity. Use a heartbeat table to force periodic LSN advancement:
|
|
In the connector config:
|
|
On PostgreSQL 14+, you can use pg_logical_emit_message() instead of a heartbeat table to avoid creating extra tables.
MySQL Deep Dive
MySQL CDC uses the binary log (binlog). Configure MySQL:
|
|
Create the Debezium user:
|
|
The MySQL connector tracks position via binlog.filename + binlog.position (and optionally GTID if enabled). Unlike PostgreSQL, there is no slot mechanism — MySQL simply doesn’t replay events prior to the connector’s recorded position. If the binlog rotates and the connector hasn’t consumed up to that point, data is lost. Keep your expire_logs_days high enough for your worst-case connector downtime.
Debezium Deployment Modes
Debezium can run in three modes:
-
Kafka Connect (recommended for production): Deploy as a Kafka Connect source connector. The Connect framework handles distributed operation, offset management, fault tolerance, and scaling. This is the standard mode.
-
Debezium Server: A standalone process that runs a Debezium engine and can route events to non-Kafka sinks (Amazon Kinesis, Google Pub/Sub, Azure Event Hubs, Redis Streams). Useful when you don’t have Kafka.
-
Embedded Engine: Run Debezium in-process within your own Java application. Useful for testing and custom integrations. Not recommended for production CDC workloads.
Schema Evolution with Schema Registry
When Debezium (or any connector) produces Avro-serialized events, the message bytes alone are not self-describing. The Confluent Schema Registry stores the schema separately and embeds a 5-byte header (magic byte + 4-byte schema ID) in each message. Consumers look up the schema ID to deserialize.
Kafka message value (bytes):
┌──────┬────────────────┬─────────────────────────────────┐
│ 0x00 │ Schema ID │ Avro payload │
│ (1B) │ (4 bytes) │ (variable length) │
└──────┴────────────────┴─────────────────────────────────┘
Converter Configuration
|
|
Other converters available:
io.confluent.connect.json.JsonSchemaConverter— JSON Schemaio.confluent.connect.protobuf.ProtobufConverter— Protobuforg.apache.kafka.connect.json.JsonConverter— plain JSON with embedded schema (no registry)org.apache.kafka.connect.storage.StringConverter— raw strings
Compatibility Modes
The Schema Registry enforces compatibility rules when a new schema version is registered. The default is BACKWARD.
| Mode | Consumers can use | Producers can use | Allowed changes |
|---|---|---|---|
BACKWARD |
New schema | Old schema | Add optional fields (with defaults), remove fields |
FORWARD |
Old schema | New schema | Remove optional fields, add fields |
FULL |
Either | Either | Only add/remove fields with defaults |
NONE |
— | — | Anything (no checking) |
BACKWARD_TRANSITIVE |
New | Any old | Backward-compatible with ALL prior versions |
FORWARD_TRANSITIVE |
Any old | New | Forward-compatible with ALL prior versions |
FULL_TRANSITIVE |
Any | Any | Fully compatible with ALL prior versions |
Upgrade order:
BACKWARD→ upgrade consumers first, then producersFORWARD→ upgrade producers first, then consumersFULL→ order doesn’t matter
Practical schema evolution rules for Avro + BACKWARD:
|
|
Checking Compatibility via Registry API
|
|
Exactly-Once Semantics
The Problem
Without EOS, a source connector can produce duplicate records when a task fails and restarts. The task publishes a batch to Kafka, the Kafka write succeeds, but the task crashes before writing the updated offsets to the offset storage topic. On restart, it replays from the last committed offset — producing duplicates.
For sink connectors, the sink itself must be idempotent for EOS to be meaningful. Kafka can guarantee exactly-once delivery to Kafka topics, but if the downstream system doesn’t support idempotent writes, you still get duplicates at the destination.
EOS for Source Connectors (KIP-618)
Available since Kafka 3.3, exactly-once source support uses Kafka transactions to atomically write source records and their corresponding offsets in a single transaction:
BEGIN TRANSACTION
→ Write records to output topic(s)
→ Write offsets to offset storage topic
COMMIT
If the task fails after the write but before the commit, the transaction is aborted. On restart, the records are never visible to consumers (they read at read_committed isolation). The offset is not advanced. The task replays from the prior committed position.
Worker configuration:
|
|
EOS source support requires:
- Kafka 3.3+ brokers
- Distributed mode only (not standalone)
- The connector must be able to provide source offsets (nearly all connectors do)
producer.enable.idempotence=true
Connector-level opt-in:
|
|
Set to required to fail fast if EOS isn’t available. Set to requested to use EOS when available but degrade gracefully.
EOS for Sink Connectors
Sink EOS is more nuanced. Connect can consume from Kafka with exactly-once guarantees (using isolation.level=read_committed and consumer group offsets committed inside transactions), but the write to the external system must also be idempotent or transactional.
Examples:
- JDBC Sink to PostgreSQL: PostgreSQL supports UPSERT (
INSERT ... ON CONFLICT DO UPDATE), which makes writes idempotent given a primary key. With this, you get effective end-to-end EOS. - S3 Sink: Object storage writes are naturally idempotent (same key overwrites same file). Partitioned by offset range, the same data written twice results in identical files.
- Elasticsearch: Supports idempotent indexing via document ID; duplicates overwrite.
Error Handling and Dead Letter Queues
Error Handling Config
|
|
errors.tolerance:
none(default): Any conversion or transformation error causes the task to fail immediately. The connector entersFAILEDstate.all: Errors are silently skipped. If a DLQ is configured, failed records are routed there. Processing continues.
Important limitation: errors.tolerance only applies to conversion and transformation errors — it does not catch connector polling failures (source) or write failures (sink). A JDBC sink that fails to write to the database will still fail the task regardless of errors.tolerance.
Retry settings:
errors.retry.timeout: Total time in ms to retry a failed operation before giving up.-1means retry forever.errors.retry.delay.max.ms: Maximum backoff delay between retries (exponential backoff).
DLQ Headers
When errors.deadletterqueue.context.headers.enable=true, each DLQ record gets headers describing the failure:
__connect.errors.topic = source-topic-name
__connect.errors.partition = 3
__connect.errors.offset = 12345
__connect.errors.connector.name = my-connector
__connect.errors.task.id = 0
__connect.errors.stage = VALUE_CONVERTER
__connect.errors.class.name = org.apache.kafka.connect.errors.DataException
__connect.errors.exception.message = Failed to deserialize data...
__connect.errors.exception.stacktrace = ...
Distributed Mode at Scale
Capacity Planning
Each worker runs connector tasks as threads inside the JVM. Key limits:
- Tasks per worker: There is no hard limit, but each task holds I/O resources (database connections, HTTP connections). 20-50 active tasks per worker is typical for database connectors. A single Debezium task can saturate a worker if the database is high-throughput.
- Memory: Allocate at least 4 GB heap for a production worker. Debezium with large batch sizes needs 8+ GB. Each task’s in-memory queue (
max.queue.size) also consumes heap. - CPU: Connect workers are mostly I/O-bound, but serialization (especially Avro) adds CPU load.
Internal Topic Best Practices
Create internal topics manually before starting Connect:
|
|
Worker JVM Tuning
|
|
Multiple Plugin Versions (Kafka 4.1)
Kafka 4.1 introduced KIP-891: you can now install multiple versions of the same connector plugin on a single worker. This allows in-place connector upgrades and downgrades without separate Connect clusters.
Plugins are isolated by version using separate classloaders. Specify the plugin version in the connector config:
|
|
Running on Kubernetes with Strimzi
Strimzi is the CNCF operator for running Kafka (and Kafka Connect) on Kubernetes. Current version: 0.51 (supports Kubernetes 1.30+, includes v1 stable CRD API introduced in 0.49).
KafkaConnect CRD
|
|
Building Custom Connector Images with spec.build
Instead of manually building Docker images, Strimzi’s spec.build feature has the operator build the image automatically using Kaniko (on Kubernetes) or OpenShift Builds:
|
|
When this resource is applied, the Strimzi Cluster Operator:
- Creates a build pod with Kaniko
- Downloads the specified artifacts
- Builds a Docker image with all plugins installed
- Pushes to the specified registry
- Updates the KafkaConnect deployment to use the new image
No manual Dockerfile required.
KafkaConnector CRD
With strimzi.io/use-connector-resources: "true" on the KafkaConnect resource, connectors are managed via KafkaConnector resources instead of direct REST API calls. The Strimzi operator reconciles KafkaConnector resources to the Connect REST API.
|
|
Secrets in KafkaConnector configs: Use the ${secrets:namespace/secret-name:key} syntax to reference Kubernetes secrets directly. The Strimzi operator injects the value; the secret never appears in the KafkaConnector YAML or Connect offset storage.
Scaling Connect Workers
Scale the number of workers by changing spec.replicas:
|
|
Strimzi performs a rolling update. Connect’s incremental cooperative rebalancing handles task redistribution with minimal disruption.
Local Development with Docker Compose
|
|
Monitoring
JMX Metrics
Kafka Connect exposes metrics via JMX. The Prometheus JMX Exporter makes these scrapable. Key MBeans:
Worker-level metrics (kafka.connect:type=connect-worker-metrics):
connector-count— number of connectors managed by this workertask-count— number of tasks on this workerconnector-startup-success-total,connector-startup-failure-totaltask-startup-success-total,task-startup-failure-total
Task-level source metrics (kafka.connect:type=source-task-metrics,connector=*,task=*):
source-record-poll-rate— records polled per second from the sourcesource-record-poll-total— total records polledsource-record-write-rate— records written to Kafka per secondsource-record-active-count— records currently in the in-memory queuepoll-batch-max-time-ms,poll-batch-avg-time-ms— poll batch latency
Task-level sink metrics (kafka.connect:type=sink-task-metrics,connector=*,task=*):
sink-record-read-rate— records read from Kafka per secondsink-record-send-rate— records sent to the sink per secondsink-record-active-count— records in flight (read but not yet committed)offset-commit-completion-rate,offset-commit-max-time-msput-batch-max-time-ms— maximum time for a sink put() call
Error metrics (kafka.connect:type=task-error-metrics,connector=*,task=*):
total-errors-loggedtotal-records-skippeddeadletterqueue-produce-requests— DLQ write attemptsdeadletterqueue-produce-failures— DLQ write failures (alert on this being > 0)
Prometheus JMX Exporter Config for Strimzi
|
|
Key Alerts
| Alert | Condition | Severity |
|---|---|---|
| Connector down | Connector state != RUNNING via REST API | Critical |
| Task failed | Task state == FAILED | Critical |
| High DLQ rate | deadletterqueue-produce-requests > 0 for sustained period |
Warning |
| Source lag | source-record-active-count near max.queue.size |
Warning |
| Sink commit slow | offset-commit-max-time-ms > 5000 |
Warning |
| PG slot lag | pg_wal_lsn_diff(current, restart_lsn) > 5 GB |
Critical |
| DLQ write failures | deadletterqueue-produce-failures > 0 |
Critical |
REST API Health Checks
|
|
Common Pitfalls
1. Replication Slot Lag (PostgreSQL)
Already covered above. The short version: if your Debezium connector stops for long enough, your PostgreSQL disk fills with unreclaimed WAL. Monitor pg_replication_slots continuously. Set max_slot_wal_keep_size. Use heartbeat queries on low-traffic databases.
2. Consumer Group Name Collisions
Every Kafka Connect worker cluster needs a unique group.id. If two Connect clusters share a group.id, they will join the same consumer group and fight over internal topic partitions, causing continuous rebalances. Name your clusters descriptively: connect-cdc-prod, connect-sink-prod, not connect-1.
3. Connector Class Not Found
ClassNotFoundException for a connector class usually means:
- The JAR is not in any directory listed in
plugin.path - The JAR is in
plugin.pathbut its dependencies are missing (put the full connector distribution, not just the main JAR) - The JAR was added after the worker started (workers scan
plugin.pathat startup; restart required) - The JAR is in the Kafka
libs/directory but not isolated — this can cause classloader conflicts; keep connector JARs in their own subdirectories underplugin.path
Use the REST API to confirm what plugins are loaded: GET /connector-plugins.
4. Offset Reset Behavior
If you delete a connector and recreate it with the same name, Connect will look up the last saved offsets for that connector name in the offset topic. It will resume from where it left off, not from the beginning. To start fresh, either use a new connector name or manually delete the offset entries from the offset storage topic (requires direct Kafka tooling — there is no REST API for this).
5. Memory Pressure from Large Batch Sizes
Source connectors buffer records in a queue before writing to Kafka. The queue is bounded by max.queue.size (default 500 records) and max.queue.bytes (default 128 MB, added in Connect 2.6). On high-throughput connectors or connectors producing large messages (e.g., blob columns in Debezium), this queue can consume significant heap. Tune max.queue.size, max.queue.bytes, and worker heap (-Xmx) together.
6. Schema Evolution Failures at Runtime
If a source connector produces a record with a schema that is incompatible with the registered schema in the Schema Registry, the converter will throw an exception and the task will fail (or the record will go to DLQ if errors.tolerance=all). The most common cause is a DDL change in the source database that Debezium detects and reflects in the Avro schema, which then fails compatibility checks.
Mitigation strategies:
- Use
FULL_TRANSITIVEcompatibility for CDC topics — it’s the strictest and safest. - Monitor the Schema Registry for new schema registrations (alert on unexpected schema changes).
- Use the Debezium
schema.name.adjustment.modeand explicitly manage your Schema Registry compatibility before making DDL changes.
7. Task Imbalance After Worker Loss
When a worker fails, its tasks are rebalanced to surviving workers. If the surviving workers are already near capacity, tasks may fail to start due to resource exhaustion (database connection limits, memory). Always provision workers with headroom. A rule of thumb: size your cluster to handle expected load with one worker down.
8. Strimzi: Connector Paused by Operator
If you set pause: true on a KafkaConnector resource, the operator pauses the connector. This is correct behavior. But if a connector enters PAUSED state unexpectedly, check whether the KafkaConnector spec has pause: true — someone may have edited it via GitOps while debugging and forgotten to restore it.
Kafka 4.0 and 4.1 Connect Changes
Kafka 4.0 (March 2025):
- ZooKeeper fully removed — KRaft is now the only mode
- Java 17 minimum for brokers, Connect, and tools
- KIP-1032: Connect upgraded from JavaEE to Jakarta EE 10 APIs — connector JARs using old
javax.*imports must be updated - Removed deprecated
GET /connectors/{connector}/tasks-configendpoint — useGET /connectors/{connector}/tasks
Kafka 4.1 (September 2025):
- KIP-891: Multiple connector plugin versions on a single worker — enables safe in-place connector upgrades
- KIP-877: Plugins and connectors can now register custom JMX metrics via their task context
Version Reference
| Component | Current Stable | Notes |
|---|---|---|
| Apache Kafka | 4.1 (Sep 2025) | KRaft-only, Java 17 minimum |
| Debezium | 3.5.0.Final (Mar 2026) | Adds CockroachDB snapshot, Oracle 23c, IBMi JDBC sink |
| Strimzi | 0.51 (2025) | v1 stable CRD API, Kubernetes 1.30+ required |
| Confluent Platform | 7.7.x | Kafka 3.7 based |
| Schema Registry (CP) | 7.7.x | Bundled with Confluent Platform |
| Confluent Hub S3 Sink | 10.5.x | Uses AWS SDK v2 (breaking from 10.x → 11.x) |
Kafka Connect is the right answer for moving data into and out of Kafka at scale — when you deploy it correctly. The framework handles more than it appears to: offset management, distributed coordination, fault tolerance, schema negotiation, and error routing are all there out of the box. The connectors themselves (especially Debezium) are mature, well-tested, and trusted in production at significant scale. The operational complexity is real but manageable once you understand what the system is actually doing: using Kafka itself as the coordination and storage layer, leaving you with only the connector logic and monitoring to reason about.
The most important things to get right in production: monitor your replication slot lag from day one if you’re using Debezium on PostgreSQL, design your schema evolution strategy before you need it, size your workers with headroom, and use Strimzi on Kubernetes to make connector lifecycle a GitOps operation rather than a series of curl commands.
Sources:
- Debezium Releases Overview
- Debezium 3.5.0.Final Released
- Debezium 3.4.0.Final Released
- Strimzi 0.49.0 v1 API Introduction
- Strimzi GitHub Releases
- Apache Kafka 4.0.0 Release Announcement
- Apache Kafka 4.1.0 Release Announcement
- KIP-618: Exactly-Once Support for Source Connectors
- KIP-415: Incremental Cooperative Rebalancing
- Confluent Schema Registry Compatibility Types
- Kafka Connect Error Handling and DLQ
- Strimzi Connector Build Feature
- Mastering Postgres Replication Slots - Gunnar Morling
- Optimizing Debezium CDC: Preventing PostgreSQL WAL Accumulation
- Kafka Connect Configs - Apache Kafka 4.2
- Strimzi Deploying and Managing
- Debezium PostgreSQL Connector Documentation
Comments