> ## 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 IllegalStateException: No Current Assignment for Partition
- URL: https://www.ggorantala.dev/kafka-no-current-assignment-for-partition-fix/
- Published: 2026-03-02T11:11:00.000Z
- Updated: 2026-08-29T08:27:15.000Z
- Description: Getting java.lang.IllegalStateException: No current assignment for partition on seek()? Why Kafka's lazy assignment causes it — and the correct fix.
- Author: Gopi Gorantala
- Tags: #kafka-consumer-groups, kafka-errors, IllegalStateException, kafka-consumer, kafka-seek, consumer-rebalance, spring-kafka, Java

You called `seek()` (or `seekToBeginning()`, `position()`, `pause()`) on a `KafkaConsumer` and got this:

```text
Exception in thread "main" java.lang.IllegalStateException: No current assignment for partition demo-topic-0
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.assignedState(SubscriptionState.java:378)
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.seekUnvalidated(SubscriptionState.java:395)
	at org.apache.kafka.clients.consumer.KafkaConsumer.seek(KafkaConsumer.java:1632)
	at com.example.ReplayConsumer.main(ReplayConsumer.java:24)

```

In Spring Kafka the same exception surfaces wrapped in listener-container startup or error-handler logs, typically when someone calls `seek` from the wrong place:

```text
java.lang.IllegalStateException: No current assignment for partition orders-3
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.assignedState(SubscriptionState.java:378)
	at org.apache.kafka.clients.consumer.internals.SubscriptionState.requestOffsetReset(SubscriptionState.java:426)
	at org.apache.kafka.clients.consumer.KafkaConsumer.seekToBeginning(KafkaConsumer.java:1680)

```

Typical setup where this bites: kafka-clients 3.x (any version — the guard has been there since 0.9, message unchanged), Spring Kafka 3.x, local Docker or managed Kafka. It is a **client-side** exception. The broker never sees the call. Line numbers vary by version; since kafka-clients 3.7 the frames may show `ClassicKafkaConsumer` or `AsyncKafkaConsumer` instead of `KafkaConsumer` — same bug, same fix.

Two distinct situations produce it:

1. **POC/startup variant** — you call `seek()` right after `subscribe()`, before the consumer has actually joined the group. This is the overwhelmingly common one.
2. **Rebalance race variant** — production consumer, worked for weeks, then a rebalance revoked the partition between polls and some code (an error handler, a stashed `TopicPartition`, a metrics endpoint calling `position()`) touched a partition the consumer no longer owns.

## 2\. How to Reproduce It (step-by-step)

Single-broker KRaft cluster:

```yaml
# docker-compose.yml
services:
  kafka:
    image: apache/kafka:3.8.0
    ports:
      - "9092:9092"
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:29093
      KAFKA_LISTENERS: PLAINTEXT://:29092,CONTROLLER://:29093,EXTERNAL://:9092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,EXTERNAL://localhost:9092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

```

```bash
docker compose up -d
docker compose exec kafka /opt/kafka/bin/kafka-topics.sh \
  --bootstrap-server localhost:29092 --create --topic demo-topic --partitions 3

```

Now the code every "replay from the beginning" tutorial victim writes (kafka-clients pinned to `3.8.0`):

```java
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "replay-poc");
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", StringDeserializer.class.getName());

try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
    consumer.subscribe(List.of("demo-topic"));

    // BOOM — throws IllegalStateException: No current assignment for partition demo-topic-0
    consumer.seek(new TopicPartition("demo-topic", 0), 0L);

    while (true) {
        consumer.poll(Duration.ofMillis(500))
                .forEach(r -> System.out.println(r.value()));
    }
}

```

Run it. It fails on the `seek()` line 100% of the time, on every Kafka version, regardless of broker state. No producer needed, no data needed.

The rebalance-race variant is reproducible too: start two instances of a consumer that caches `TopicPartition` objects from a previous `poll()` and later calls `consumer.position(cachedTp)`. When instance #2 joins and the group rebalances, the cached partition may move to the other instance, and the next `position()` call on it throws the same exception. It only fires under consumer churn — which is why it shows up during deploys and autoscaling events, not in your single-instance dev run.

## 3\. Why It Happens — Surface Level

`subscribe()` does not assign you anything. It records an intent. Partition assignment for a subscribed consumer only materializes after the consumer joins the group — and joining the group happens **inside `poll()`**, not inside `subscribe()`.

So between `subscribe()` and the first successful `poll()` that completes the group join, the consumer's assignment set is empty. `seek()`, `seekToBeginning()`, `seekToEnd()`, `position()`, `pause()`, and `resume()` all operate on the *current assignment*, and each of them throws `IllegalStateException: No current assignment for partition X` if you pass a partition that isn't in that set. Note the asymmetry: `committed()` does *not* require assignment (it's a coordinator lookup), which is why half your offset code works and the other half explodes.

## 4\. Why It Happens — Under the Hood

Every `KafkaConsumer` holds a `SubscriptionState` object — the client-side source of truth for what you subscribed to, what you're assigned, and the fetch position per partition. Position-mutating APIs funnel through `SubscriptionState.assignedState(tp)`:

```java
// kafka-clients internals (abridged)
private TopicPartitionState assignedState(TopicPartition tp) {
    TopicPartitionState state = this.assignment.stateValue(tp);
    if (state == null)
        throw new IllegalStateException("No current assignment for partition " + tp);
    return state;
}

```

For a `subscribe()`\-mode consumer, `this.assignment` is populated only when the group protocol completes: `poll()` drives `ConsumerCoordinator`, which sends `JoinGroup`, waits for the leader to run the partition assignor, receives its share via `SyncGroup`, and *then* installs the assignment and fires `onPartitionsAssigned`. Until that handshake finishes, the assignment map is empty and `assignedState()` throws.

Two details make this nastier than it looks:

**`poll()` completing is not a synchronization point you can rely on.** Since KIP-266, `poll(Duration)` respects the timeout: `poll(Duration.ZERO)` returns immediately, possibly before the join completes. The old "call `poll(0)` once, then seek" hack is a race condition wearing a trench coat — it worked with the deprecated `poll(long)` (which blocked indefinitely on coordinator operations) and silently broke when people migrated to `poll(Duration)`.

**Assignment is revocable at any poll boundary.** With the default `RangeAssignor`/eager rebalancing, *every* rebalance revokes *all* partitions before reassigning; with `CooperativeStickyAssignor` only migrating partitions are revoked, but revocations still happen. Any `TopicPartition` reference you cached is a claim ticket that can expire between two `poll()` calls. Code that seeks/pauses partitions from a stale snapshot — a common pattern in retry/backoff handlers that pause partitions and resume them from a scheduled task on another data structure — throws exactly this exception the first time a rebalance lands mid-backoff. (If your logs show this error clustered around rebalance log lines, this is your variant.)

The consumer group protocol is being rewritten by KIP-848 (broker-driven assignment, default in Kafka 4.0's new consumer), but the client contract is unchanged: assignment still only materializes through `poll()`, and position APIs still throw on unassigned partitions.

## 5\. The Fix

### Fix 1: seek inside `onPartitionsAssigned` (subscribe mode — the right way)

The `ConsumerRebalanceListener` callback runs at the only moment assignment is guaranteed current:

```diff
- consumer.subscribe(List.of("demo-topic"));
- consumer.seek(new TopicPartition("demo-topic", 0), 0L);   // throws
+ consumer.subscribe(List.of("demo-topic"), new ConsumerRebalanceListener() {
+     @Override
+     public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
+         consumer.seekToBeginning(partitions);   // assignment is live here
+     }
+     @Override
+     public void onPartitionsRevoked(Collection<TopicPartition> partitions) { }
+ });

  while (true) {
      consumer.poll(Duration.ofMillis(500)).forEach(...);
  }

```

The callback executes on the polling thread inside `poll()`, after assignment is installed — no race, and it re-runs after every rebalance, so your seek logic also survives partition migration. If you only want to rewind on first assignment (not on every rebalance), keep a `Set<TopicPartition>` of already-seeked partitions and skip them.

### Fix 2: use `assign()` when you don't want group management

If you're building a replay tool, a backfill job, or a consumer with an external offset store, you don't want a consumer group deciding your assignment at all:

```diff
- consumer.subscribe(List.of("demo-topic"));
+ List<TopicPartition> parts = consumer.partitionsFor("demo-topic").stream()
+         .map(pi -> new TopicPartition(pi.topic(), pi.partition()))
+         .toList();
+ consumer.assign(parts);
  consumer.seekToBeginning(parts);   // legal immediately — assign() is synchronous

```

`assign()` populates `SubscriptionState` directly, client-side, with no group join. Seek works on the very next line. Use this for tooling and one-off jobs; use subscribe mode for services that need scaling and failover. Never mix the two on one consumer instance — that throws its own `IllegalStateException` ("Subscription to topics, partitions and pattern are mutually exclusive").

### Fix 3: Spring Kafka — `AbstractConsumerSeekAware`

Don't touch the `Consumer` from outside the container thread. Spring routes the same rebalance callback for you:

```java
@Component
public class ReplayListener extends AbstractConsumerSeekAware {

    @Override
    public void onPartitionsAssigned(Map<TopicPartition, Long> assignments,
                                     ConsumerSeekCallback callback) {
        // runs on the consumer thread, assignment guaranteed current
        assignments.keySet().forEach(tp -> callback.seekToBeginning(tp.topic(), tp.partition()));
    }

    @KafkaListener(topics = "demo-topic", groupId = "replay-poc")
    public void listen(String value) {
        // ...
    }
}

```

For ad-hoc rewinds at runtime (ops endpoint, admin trigger), `AbstractConsumerSeekAware` exposes `seekToBeginning()` / `seekToTimestamp(long)` on the listener bean itself; Spring queues the seek and applies it safely on the consumer thread. Calling `consumer.seek()` from an HTTP handler thread will get you either this exception or a `ConcurrentModificationException` — both are the same design error.

### Fix for the rebalance-race variant

Never cache `TopicPartition` sets across polls for position operations. Re-derive from `consumer.assignment()` at the point of use, and treat the operation as best-effort:

```diff
- consumer.pause(stashedPartitions);                    // stale snapshot → throws
+ Set<TopicPartition> owned = new HashSet<>(consumer.assignment());
+ stashedPartitions.retainAll(owned);
+ consumer.pause(stashedPartitions);                    // only what we still own

```

If a partition was revoked, whoever owns it now is responsible for it — pausing or seeking it from the old owner was never meaningful anyway.

## 6\. Best Practices & The Better Design

The class of bug here is **treating assignment as a static fact instead of a leased, revocable state**. The design that avoids it:

```java
// The pattern: all position state derives from the rebalance listener, nothing cached elsewhere
public class ReplayableConsumer implements ConsumerRebalanceListener, AutoCloseable {

    private final KafkaConsumer<String, String> consumer;
    private final long replayFromTimestamp;

    public ReplayableConsumer(Properties props, long replayFromTimestamp) {
        props.put(ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
                  CooperativeStickyAssignor.class.getName());
        this.consumer = new KafkaConsumer<>(props);
        this.replayFromTimestamp = replayFromTimestamp;
    }

    @Override
    public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
        Map<TopicPartition, Long> query = partitions.stream()
                .collect(Collectors.toMap(tp -> tp, tp -> replayFromTimestamp));
        consumer.offsetsForTimes(query).forEach((tp, oat) -> {
            if (oat != null) consumer.seek(tp, oat.offset());   // else: no data past ts, keep committed position
        });
    }

    @Override
    public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
        // commit or flush per-partition state here, then forget these partitions
    }

    public void run(String topic) {
        consumer.subscribe(List.of(topic), this);
        while (true) {
            consumer.poll(Duration.ofMillis(500)).forEach(this::process);
        }
    }
    // ...
}

```

Why this shape: `offsetsForTimes` \+ seek inside the callback gives you timestamp-based replay that is correct across rebalances; `CooperativeStickyAssignor` minimizes how often partitions actually move; revocation is the flush point for any per-partition state. Add `group.instance.id` (static membership) on Kubernetes so restarts within `session.timeout.ms` don't trigger rebalances at all — fewer assignment changes, fewer chances for stale-partition bugs.

And a team convention worth enforcing in review: **`seek`/`pause`/`resume`/`position` may only appear inside a `ConsumerRebalanceListener` callback, immediately after `assign()`, or guarded by `consumer.assignment().contains(tp)`.** Anywhere else is a latent production incident.

## 7\. How to Prevent It Long-Term

**Test with rebalances, not against them.** A Testcontainers integration test that starts one consumer, lets it stabilize, then starts a second instance of the same group will flush out every stale-`TopicPartition` bug in your seek/pause logic. Single-consumer tests never rebalance and therefore never catch this.

**Monitor rebalance rate.** The client-side metrics `rebalance-rate-per-hour` and `failed-rebalance-rate-per-hour` (consumer-coordinator-metrics group) tell you how often assignment is churning. If this exception appears in logs, correlate timestamps with rebalances — the race variant always co-occurs.

**Grep your codebase.** `consumer.seek`, `.pause(`, `.position(` outside a rebalance listener or `assign()` block is a five-minute audit. In Spring services, any injected/stashed `Consumer` reference used from a non-listener thread is a finding.

**Lint the poll(0) hack away.** If you find `poll(Duration.ZERO)` followed by `seek`, it's the legacy workaround and it's racy. Replace with a rebalance listener.

## 8\. Key Takeaways

- `subscribe()` assigns nothing — assignment materializes inside `poll()` after the group join, so `seek()` right after `subscribe()` always throws `IllegalStateException: No current assignment for partition`.
- `seek`, `seekToBeginning/End`, `position`, `pause`, `resume` require current assignment; `committed()` doesn't — that asymmetry is why the bug looks intermittent.
- Do seeks inside `ConsumerRebalanceListener.onPartitionsAssigned` (or Spring's `AbstractConsumerSeekAware`); use `assign()` for group-less replay/backfill tools.
- Never cache `TopicPartition` sets across polls — assignment is a lease that any rebalance can revoke; re-check `consumer.assignment()` at the point of use.
- The `poll(0)`\-then-seek hack is a race since KIP-266's `poll(Duration)` — if you see it, replace it.