apache/kafka · error · java.lang.IllegalArgumentException

seek offset must not be a negative number

Error message

seek offset must not be a negative number

What it means

IllegalArgumentException thrown at the entry of AsyncKafkaConsumer.seek(TopicPartition, long) when the supplied offset is < 0. Offsets are non-negative monotonically increasing positions within a partition; a negative offset has no valid meaning and is rejected before any network I/O. This guard runs before acquireAndEnsureOpen(), so it fires regardless of consumer state.

Source

Thrown at clients/src/main/java/org/apache/kafka/clients/consumer/internals/AsyncKafkaConsumer.java:1156

        throwIfGroupIdNotDefined();
        offsetCommitCallbackInvoker.executeCallbacks();

        if (commitEvent.offsets().isPresent() && commitEvent.offsets().get().isEmpty()) {
            return CompletableFuture.completedFuture(null);
        }

        applicationEventHandler.add(commitEvent);

        // This blocks until the background thread retrieves allConsumed positions to commit if none were explicitly specified.
        // This operation will ensure that the offsets to commit are not affected by fetches which may start after this
        ConsumerUtils.getResult(commitEvent.offsetsReady(), defaultApiTimeoutMs.toMillis());
        return commitEvent.future();
    }

    @Override
    public void seek(TopicPartition partition, long offset) {
        if (offset < 0)
            throw new IllegalArgumentException("seek offset must not be a negative number");

        acquireAndEnsureOpen();
        try {
            log.info("Seeking to offset {} for partition {}", offset, partition);
            SeekUnvalidatedEvent seekUnvalidatedEventEvent = new SeekUnvalidatedEvent(
                defaultApiTimeoutDeadlineMs(),
                partition,
                offset,
                Optional.empty()
            );
            applicationEventHandler.addAndGet(seekUnvalidatedEventEvent);
        } finally {
            release();
        }
    }

    @Override
    public void seek(TopicPartition partition, OffsetAndMetadata offsetAndMetadata) {

View on GitHub (pinned to c31c9215e1)

Solutions

  1. Validate the offset is >= 0 before calling seek; if it is -1 or unknown, call seekToBeginning/seekToEnd or skip the seek.
  2. Fix the upstream offset computation: clamp at 0, or branch when the source offset is uninitialized.
  3. Replace sentinel-style usage (-1, -2) with the dedicated seekToBeginning(Collection) / seekToEnd(Collection) APIs.

Example fix

// before
long offset = committedOffset != null ? committedOffset - lookback : -1;
consumer.seek(tp, offset);

// after
if (committedOffset == null) {
    consumer.seekToBeginning(List.of(tp));
} else {
    consumer.seek(tp, Math.max(0, committedOffset - lookback));
}
Defensive patterns

Strategy: type-guard

Validate before calling

// seek(TopicPartition, long) — guard the offset at the call site.
static void safeSeek(Consumer<?, ?> c, TopicPartition tp, long offset) {
    if (offset < 0)
        throw new IllegalArgumentException("seek offset for " + tp + " is negative: " + offset);
    c.seek(tp, offset);
}

// If the offset originates from external state (DB, file), coerce defensively:
long parsed = Long.parseLong(stored);
if (parsed < 0) throw new IllegalStateException("Stored offset corrupt (negative): " + stored);
safeSeek(consumer, tp, parsed);

Type guard

// Use a non-negative wrapper type so the compiler rejects bad values at the source.
// In Java, a small value class with a static factory that validates:
public final class NonNegativeOffset {
    private final long value;
    private NonNegativeOffset(long v) { this.value = v; }
    public static NonNegativeOffset of(long v) {
        if (v < 0) throw new IllegalArgumentException("offset must be >= 0, got " + v);
        return new NonNegativeOffset(v);
    }
    public long value() { return value; }
}

// TypeScript analogue:
//   type NonNegativeLong = number & { __brand: 'NonNegative' };
//   function nonNegative(n: number): NonNegativeLong {
//     if (!Number.isInteger(n) || n < 0) throw new RangeError('negative offset');
//     return n as NonNegativeLong;
//   }
//   function seek(c: Consumer, tp: TopicPartition, off: NonNegativeLong): void { c.seek(tp, off); }

Try / catch

// IllegalArgumentException from seek is a programmer error; catch only to enrich
// diagnostics, then propagate (do not retry the same value).
try {
    consumer.seek(tp, offset);
} catch (IllegalArgumentException e) {
    throw new IllegalStateException(
        "Refusing to seek " + tp + " to offset " + offset + "; check the source of this offset", e);
}

Prevention

When it happens

Trigger: Calling consumer.seek(partition, -1) or passing a computed offset that became negative (e.g. committedOffset - N with N > committedOffset, or defaulting an unknown/uninitialized offset to -1). Also triggered by seek to a sentinel like -2 / -1 that older code used to mean 'beginning'/'end' (use seekToBeginning/seekToEnd instead).

Common situations: Offset arithmetic bugs (committedOffset - lookback where lookback exceeds committed); using -1 as a 'not yet set' placeholder that leaks into seek; porting code from an API that accepted negative sentinels; caching layer returning -1 on miss and feeding it to seek; off-by-one in tests.

Related errors


AI-assisted analysis of apache/kafka@c31c9215e1 (2026-08-03). Data as JSON: /data/errors/44f0bd9eb5329cfa.json. Report an issue: GitHub.