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
- Query min/max first via consumer.minOffset(q)/maxOffset(q) and clamp: Math.min(Math.max(offset, min), max)
- For seek-to-tail use maxOffset(q); for seek-to-head use minOffset(q) instead of literals
- 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
- Never hard-code offsets; always clamp against current min/max before seek
- Use minOffset(q)/maxOffset(q) for head/tail seeks instead of 0/Long.MAX_VALUE
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
- The message queue is not in assigned list, may be rebalancin
- The message queue is not in assigned list, message queue:
- Fetch consume offset from broker exception
- offset < 0
- QUERY_NOT_FOUND
AI-assisted analysis of apache/rocketmq@293f588571 (2026-08-14).
Data as JSON: /api/errors/860410b68c2e0e54.
Report an issue: GitHub.