Skip to content

Kafka Connect: Failed to Find Any Class That Implements Connector

Kafka Connect throws 'Failed to find any class that implements Connector' when plugin.path is wrong or classloader isolation hides your JAR. Here's the fix.

Gopi Gorantala
Gopi Gorantala
9 min read
Reading Progress

On This Page

You built or downloaded a connector, dropped the JAR somewhere, POSTed a config to the REST API, and got a 500. Or the worker logged Scanning for plugin classes on startup and still can't find the one class you actually need. This is the single most common Kafka Connect onboarding failure, and 90% of the time it has nothing to do with your connector code — it's a plugin.path and classloader-isolation problem.

This is the third Kafka Connect article in the series. It is mechanically distinct from the converter poison-pill failure (a task dies at runtime on bad bytes) and the source offset-flush timeout (a running task can't keep up). This one fails before a task ever starts — the worker can't even instantiate the connector class.

1. The Error

From the REST API when you create a connector:

{
  "error_code": 500,
  "message": "Failed to find any class that implements Connector and which name matches io.confluent.connect.jdbc.JdbcSourceConnector, available connectors are: PluginDesc{klass=class org.apache.kafka.connect.file.FileStreamSinkConnector, name='org.apache.kafka.connect.file.FileStreamSinkConnector', version='3.9.0', ...}, PluginDesc{klass=class org.apache.kafka.connect.mirror.MirrorSourceConnector, ...}"
}

The same thing shows up in the worker log as a ConnectException, wrapped through the REST layer:

org.apache.kafka.connect.errors.ConnectException: Failed to find any class that implements Connector
 and which name matches io.confluent.connect.jdbc.JdbcSourceConnector, available connectors are: [...]
	at org.apache.kafka.connect.runtime.isolation.Plugins.connectorClass(Plugins.java:246)
	at org.apache.kafka.connect.runtime.isolation.Plugins.newConnector(Plugins.java:213)
	at org.apache.kafka.connect.runtime.AbstractHerder.getConnector(AbstractHerder.java:750)
	at org.apache.kafka.connect.runtime.AbstractHerder.validateConnectorConfig(AbstractHerder.java:439)

The tell is the available connectors are: list. The worker is telling you exactly which plugins it did load. If your connector isn't in that list, the worker never scanned the directory that contains it — or scanned it and couldn't isolate the JAR.

Environment where this bites: Kafka Connect 2.x–4.0 (KRaft or ZooKeeper brokers, doesn't matter — Connect is a client), standalone or distributed, self-managed Docker/k8s or Confluent Platform images. Versions below assume kafka-connect bundled with Apache Kafka 3.9.0 / Confluent Platform 7.9.0 unless noted.

2. How to Reproduce It

Minimal distributed worker with a deliberately empty plugin path:

# docker-compose.yml — Kafka Connect that can't find the JDBC connector
services:
  kafka:
    image: apache/kafka:3.9.0
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0

  connect:
    image: confluentinc/cp-kafka-connect:7.9.0
    depends_on: [kafka]
    ports: ["8083:8083"]
    environment:
      CONNECT_BOOTSTRAP_SERVERS: kafka:9092
      CONNECT_GROUP_ID: demo
      CONNECT_CONFIG_STORAGE_TOPIC: _connect-configs
      CONNECT_OFFSET_STORAGE_TOPIC: _connect-offsets
      CONNECT_STATUS_STORAGE_TOPIC: _connect-status
      CONNECT_CONFIG_STORAGE_REPLICATION_FACTOR: 1
      CONNECT_OFFSET_STORAGE_REPLICATION_FACTOR: 1
      CONNECT_STATUS_STORAGE_REPLICATION_FACTOR: 1
      CONNECT_KEY_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      CONNECT_VALUE_CONVERTER: org.apache.kafka.connect.json.JsonConverter
      CONNECT_REST_ADVERTISED_HOST_NAME: connect
      # The trap: points at a directory that has no JDBC connector in it
      CONNECT_PLUGIN_PATH: /usr/share/java

Bring it up and confirm what the worker actually loaded:

docker compose up -d
# Wait for the REST API, then list discovered plugins
curl -s localhost:8083/connector-plugins | jq -r '.[].class' | sort

You'll see the built-in file and mirror connectors, maybe a few converters — but not JdbcSourceConnector, because the cp-kafka-connect base image ships JDBC under /usr/share/confluent-hub-components, not /usr/share/java. Now trigger the exact error:

curl -s -X POST localhost:8083/connectors \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "jdbc-src",
    "config": {
      "connector.class": "io.confluent.connect.jdbc.JdbcSourceConnector",
      "tasks.max": "1",
      "connection.url": "jdbc:postgresql://db:5432/app",
      "topic.prefix": "pg-"
    }
  }' | jq

Response: 500 ... Failed to find any class that implements Connector and which name matches io.confluent.connect.jdbc.JdbcSourceConnector.

The reproduction is important because it shows the failure is a discovery problem, not a config problem. The connector class name is spelled correctly; the worker simply never scanned the directory that JAR lives in.

3. Why It Happens — Surface Level

Kafka Connect does not resolve connector.class off the JVM CLASSPATH the way an ordinary Java app resolves a class. It resolves it against the set of plugins it discovered by scanning plugin.path at startup. If the JAR (and all of its dependencies) isn't inside a directory listed in plugin.path, the connector is invisible, no matter where else it sits on disk.

The four things that produce this error:

  1. plugin.path doesn't include the directory that holds the connector.
  2. The connector's JAR is on plugin.path but its transitive dependencies aren't beside it, so the plugin fails to load and gets dropped silently (only a WARN on startup).
  3. The JAR is nested wrong — plugin.path must point at the parent directory that contains connector sub-directories (or uber-JARs), not at the JAR itself.
  4. A typo or version mismatch in connector.class (e.g. JdbcSourceConnector vs JdbcSinkConnector, or a class that was renamed between connector releases).

4. Why It Happens — Under the Hood

Connect uses a two-tier classloader design (KIP-146, since Kafka 0.11) specifically so that two connectors with conflicting dependency versions can coexist in one worker JVM. Understanding it is the difference between guessing and knowing.

DelegatingClassLoader is the worker's plugin registry. On startup it walks every entry in plugin.path. For each entry it treats each immediate sub-directory (and each standalone uber-JAR) as one isolated plugin. For every such plugin it creates a dedicated PluginClassLoader, a child-first classloader rooted at that directory's JARs. It then scans those JARs for classes implementing Connector, Converter, Transformation, Predicate, and friends, and records each as a PluginDesc — that's the list you see in available connectors are:.

Two consequences fall directly out of this design:

  • Child-first isolation means dependencies must be co-located. A PluginClassLoader looks in its own directory first, then delegates to the framework. If your connector needs postgresql-42.7.x.jar and it isn't in the same plugin directory, the class scan hits a NoClassDefFoundError while loading the connector class, the plugin is discarded, and you get a startup WARN — not a hard failure. The worker boots fine and the connector just isn't in the list. This is why "the JAR is right there" and "the connector isn't found" are both true at once.
  • plugin.path is a list of parent directories, not JARs. Point it at /opt/connectors, and Connect expects /opt/connectors/kafka-connect-jdbc/*.jar, /opt/connectors/debezium-postgres/*.jar, etc. Point it directly at a JAR and that plugin won't be isolated correctly. Point it at a directory whose JARs are loose (not in per-connector sub-directories) and it still works for uber-JARs, but a connector split across multiple loose JARs with conflicting deps will collide.

When you POST a connector, AbstractHerder.validateConnectorConfig calls Plugins.newConnectorPlugins.connectorClass, which looks the requested connector.class up in the DelegatingClassLoader's registry of discovered PluginDesc entries. Miss → ConnectException: Failed to find any class that implements Connector. The lookup is name-based and also honors aliases (the short JdbcSourceConnector resolves to the FQCN), so a near-miss on the name lands here too.

Plugin discovery mode (KIP-898, Kafka 3.6+). Historically the worker discovered plugins by reflectively scanning every class on plugin.path at startup — expensive and slow. KIP-898 added a plugin.discovery worker setting with modes ONLY_SCAN, HYBRID_WARN (the default from 3.6), HYBRID_FAIL, and SERVICE_LOAD. In the SERVICE_LOAD path, plugins are found via ServiceLoader manifests (META-INF/services) instead of a full class scan. The practical trap: a third-party connector built before KIP-898 has no ServiceLoader manifest, so under SERVICE_LOAD it becomes invisible and you get this exact error — even though the JAR is correctly placed. Under HYBRID_WARN you'll see a startup warning naming the plugin that lacks a manifest.

5. The Fix

Fix A — point plugin.path at the parent directory, with dependencies co-located

The JDBC connector reproduction, corrected. In the Confluent image the connector already ships under /usr/share/confluent-hub-components; include it:

   connect:
     image: confluentinc/cp-kafka-connect:7.9.0
     environment:
-      CONNECT_PLUGIN_PATH: /usr/share/java
+      CONNECT_PLUGIN_PATH: /usr/share/java,/usr/share/confluent-hub-components

For a hand-installed connector, lay it out as an isolated plugin directory with all its JARs inside:

# Correct layout — each connector in its own sub-directory under a plugin.path entry
/opt/kafka/plugins/
├── kafka-connect-jdbc/
│   ├── kafka-connect-jdbc-10.8.0.jar
│   ├── postgresql-42.7.4.jar        # transitive dep MUST live here
│   └── ... (all other deps)
└── debezium-connector-postgres/
    ├── debezium-connector-postgres-2.7.0.Final.jar
    └── ... (all deps)
# connect-distributed.properties
plugin.path=/opt/kafka/plugins

Verify before you POST anything — never let createConnector be your discovery test:

curl -s localhost:8083/connector-plugins | jq -r '.[] | "\(.type)\t\(.class)"'

If the class isn't in that list, no connector config will ever succeed. Fix the path, not the config.

Fix B — install via confluent-hub so dependencies come with it

Hand-copying a single JAR is the #1 source of the silent missing-dependency variant. Let the installer resolve the full dependency set into an isolated directory:

confluent-hub install --no-prompt \
  --component-dir /opt/kafka/plugins \
  --worker-configs /etc/kafka/connect-distributed.properties \
  confluentinc/kafka-connect-jdbc:10.8.0

Fix C — the SERVICE_LOAD / KIP-898 variant

If the connector is correctly placed but still invisible on a 3.6+ worker, check your discovery mode. A connector without a ServiceLoader manifest disappears under SERVICE_LOAD:

# connect-distributed.properties
- plugin.discovery=SERVICE_LOAD
+ plugin.discovery=HYBRID_WARN

HYBRID_WARN scans (so legacy plugins are found) and prints a warning naming any plugin that lacks a manifest. Either keep HYBRID_WARN, or run the migration tool the KIP ships and then move to SERVICE_LOAD:

# Adds ServiceLoader manifests to plugins in-place so SERVICE_LOAD can find them
connect-plugin-path.sh sync-manifests --plugin-path /opt/kafka/plugins --dry-run
connect-plugin-path.sh sync-manifests --plugin-path /opt/kafka/plugins

When to use which

Symptom Fix
Connector not in /connector-plugins at all; you copied a JAR by hand A + B (co-locate deps, prefer installer)
WARN at startup: "error loading plugin" / NoClassDefFoundError A (missing transitive dependency)
Correct on a <3.6 worker, missing after upgrade to 3.6+ C (discovery mode / manifests)
connector.class differs from the FQCN in the plugin list Fix the config string

6. Best Practices & The Better Design

Stop treating plugin.path as a place to scatter JARs. Treat it as an immutable, versioned artifact directory that is built once and mounted read-only. The connector image should be constructed at build time, not assembled by hand on a running worker:

# Dockerfile — bake connectors into an isolated plugin layout, verified at build time
FROM confluentinc/cp-kafka-connect:7.9.0

ENV CONNECT_PLUGIN_PATH=/usr/share/java,/opt/kafka/plugins

RUN confluent-hub install --no-prompt \
      --component-dir /opt/kafka/plugins \
      confluentinc/kafka-connect-jdbc:10.8.0 && \
    confluent-hub install --no-prompt \
      --component-dir /opt/kafka/plugins \
      debezium/debezium-connector-postgresql:2.7.0

Never add connector JARs to Connect's own CLASSPATH or drop them into share/java/kafka/. That loads them in the framework classloader with no isolation — it "works" until two connectors need different versions of the same library, and then you get impossible-to-debug LinkageError/ClassCastException failures at runtime. Isolation via plugin.path exists precisely to prevent that; bypassing it trades one clear startup error for a class of runtime landmines.

For fleets, standardize plugin.discovery=SERVICE_LOAD and only run connectors that ship ServiceLoader manifests (all Apache- and Confluent-built plugins do on current versions). Startup is dramatically faster because the worker stops loading every class on the path just to find the handful that are plugins.

7. How to Prevent It Long-Term

Make discovery a gated, observable step rather than a surprise at connector-create time.

  • Alert on the discovery gap, not the 500. Scrape /connector-plugins on a schedule and alert if the expected set shrinks — a bad image rollout removes a connector long before someone tries to create it. Also watch worker startup logs for Loading plugin WARNs; those name a plugin that failed to load because of a missing dependency.
  • Pin connector versions in config-as-code. The confluent-hub install lines belong in the Dockerfile / Helm values, code-reviewed, not typed on a live pod. A connector class renamed between versions (it happens) becomes a reviewable diff instead of a 2 a.m. incident.
  • Set plugin.discovery explicitly in worker config so an Apache Kafka upgrade that changes the default can't silently change which plugins are visible.
  • Keep plugin.path off the JVM CLASSPATH. Lint your worker config and base image to reject connector JARs under share/java/kafka/ or on CLASSPATH.

CI smoke test that boots the actual worker image and asserts every connector you intend to run is present, before the image is promoted:

EXPECTED="io.confluent.connect.jdbc.JdbcSourceConnector io.debezium.connector.postgresql.PostgresConnector"
got=$(curl -s localhost:8083/connector-plugins | jq -r '.[].class')
for c in $EXPECTED; do
  echo "$got" | grep -qx "$c" || { echo "MISSING PLUGIN: $c"; exit 1; }
done

8. Key Takeaways

  • Failed to find any class that implements Connector is a discovery failure, not a config failure — the worker never registered a plugin matching connector.class. Read the available connectors are: list; if yours isn't there, fix the path.
  • plugin.path is a list of parent directories of isolated plugins, resolved by scanning — not the JVM CLASSPATH. Point it at the directory that contains connector sub-directories/uber-JARs.
  • Connect uses child-first PluginClassLoader isolation, so a connector's transitive dependencies must sit in the same plugin directory; a missing dep produces a silent startup WARN and an invisible connector.
  • Prefer confluent-hub install (or a build-time Dockerfile) over hand-copying JARs so dependencies are always co-located; verify with GET /connector-plugins before creating a connector.
  • On Kafka 3.6+, plugin.discovery (KIP-898) can hide manifest-less legacy connectors under SERVICE_LOAD; use HYBRID_WARN or run connect-plugin-path.sh sync-manifests.
apache-kafkakafka-errorsConnectExceptionplugin-pathclassloaderJavaevent-streamingkafka-connect

Gopi Gorantala Twitter

I'm Gopi — 15+ years in Java, building Kafka and Flink platforms for banks, where one lost event is a financial discrepancy. I write javahandbook.com because the guides I needed didn't exist. Everything here is tested against a real cluster first.

Comments