apache/rocketmq · error · MQClientException

Seek offset illegal, seek offset =

Error message

Seek offset illegal, seek offset = 

What it means

seek() validates the requested offset against the queue's current [minOffset, maxOffset] range (queried from the broker) and throws this MQClientException when offset falls outside it. Seeking before the earliest retained offset or past the latest written offset would skip or replay data incorrectly, so it is rejected.

Source

Thrown at client/src/main/java/org/apache/rocketmq/client/impl/consumer/DefaultLitePullConsumerImpl.java:677

        assignedMessageQueue.pause(messageQueues);
    }

    public void resume(Collection<MessageQueue> messageQueues) {
        assignedMessageQueue.resume(messageQueues);
    }

    public synchronized void seek(MessageQueue messageQueue, long offset) throws MQClientException {
        if (!assignedMessageQueue.getAssignedMessageQueues().contains(messageQueue)) {
            if (subscriptionType == SubscriptionType.SUBSCRIBE) {
                throw new MQClientException("The message queue is not in assigned list, may be rebalancing, message queue: " + messageQueue, null);
            } else {
                throw new MQClientException("The message queue is not in assigned list, message queue: " + messageQueue, null);
            }
        }
        long minOffset = minOffset(messageQueue);
        long maxOffset = maxOffset(messageQueue);
        if (offset < minOffset || offset > maxOffset) {
            throw new MQClientException("Seek offset illegal, seek offset = " + offset + ", min offset = " + minOffset + ", max offset = " + maxOffset, null);
        }
        final Object objLock = messageQueueLock.fetchLockObject(messageQueue);
        synchronized (objLock) {
            clearMessageQueueInCache(messageQueue);

            PullTaskImpl oldPullTaskImpl = this.taskTable.get(messageQueue);
            if (oldPullTaskImpl != null) {
                oldPullTaskImpl.tryInterrupt();
                this.taskTable.remove(messageQueue);
            }
            assignedMessageQueue.setSeekOffset(messageQueue, offset);
            if (!this.taskTable.containsKey(messageQueue)) {
                PullTaskImpl pullTask = new PullTaskImpl(messageQueue);
                this.taskTable.put(messageQueue, pullTask);
                this.scheduledThreadPoolExecutor.schedule(pullTask, 0, TimeUnit.MILLISECONDS);
            }
        }
    }

View on GitHub (pinned to 293f588571)

Solutions

  1. Query min/max first via consumer.minOffset(q)/maxOffset(q) and clamp: Math.min(Math.max(offset, min), max)
  2. For seek-to-tail use maxOffset(q); for seek-to-head use minOffset(q) instead of literals
  3. If an old offset expired, accept replay from minOffset

Example fix

// before
consumer.seek(queue, savedOffset);

// after
long min = consumer.minOffset(queue), max = consumer.maxOffset(queue);
long target = Math.min(Math.max(savedOffset, min), max);
consumer.seek(queue, target);
Defensive patterns

Strategy: validation

Validate before calling

long min = consumer.minOffset(q), max = consumer.maxOffset(q);
long target = Math.min(Math.max(desiredOffset, min), max);
consumer.seek(q, target);

Prevention

When it happens

Trigger: seek(q, 0) when the earliest message has been deleted (minOffset advanced by retention); seek(q, Long.MAX_VALUE) intending 'end' — maxOffset is the last message offset, not last+1, so maxOffset is allowed but maxOffset+N is not.

Common situations: Seek-to-beginning on a topic whose old segments expired; hard-coding an offset captured days ago after retention deleted it; seeking to maxOffset+1 to go to tail.

Related errors


AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14). Data as JSON: /api/errors/860410b68c2e0e54. Report an issue: GitHub.