> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Kafka Connect: Failed to Find Any Class That Implements Connector
- URL: https://www.ggorantala.dev/kafka-connect-failed-to-find-class-implements-connector-fix/
- Published: 2026-02-24T14:18:00.000Z
- Updated: 2026-08-29T08:19:44.000Z
- Description: 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.
- Author: Gopi Gorantala
- Tags: kafka-errors, ConnectException, plugin-path, classloader, Java, event-streaming, kafka-connect

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](kafka-connect-task-threw-uncaught-unrecoverable-exception.md) (a task dies at runtime on bad bytes) and the [source offset-flush timeout](kafka-connect-failed-to-flush-offset-commit-timeout-fix.md) (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:

```json
{
  "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:

```yaml
# 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:

```bash
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:

```bash
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.newConnector` → `Plugins.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:

```diff
   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:

```bash
# 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)

```

```properties
# connect-distributed.properties
plugin.path=/opt/kafka/plugins

```

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

```bash
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:

```bash
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`:

```diff
# 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`:

```bash
# 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
# 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:

```bash
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`.