redis/node-redis · error · Error
SCAN: unknown cursor
Error message
SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN cursors are minted per client instance and expire when idle — restart the scan from 0. What it means
The cursor token passed to a cluster-wide SCAN does not match any client-minted cursor. In cluster mode, SCAN cursors are opaque virtual tokens created per client instance; they expire when idle and are not shared across instances. A stale or foreign cursor cannot be resolved to a (node, real-cursor) pair.
Solutions
- Restart the scan from cursor '0' when this error is caught
- Avoid long delays between SCAN pages — process pages promptly
- Increase SCAN COUNT to reduce round trips and complete faster
- Do not persist cursors across application restarts or share across client instances
Defensive patterns
Strategy: retry
Try / catch
async function scanFromZero(client, fn) {
for (;;) {
try {
let cursor = '0';
do {
const reply = await client.scan(cursor);
await fn(reply.keys);
cursor = reply.cursor;
} while (cursor !== '0');
return;
} catch (e) {
if (e.message.startsWith('SCAN: unknown cursor')) {
continue; // restart from 0
}
throw e;
}
}
} Prevention
- Always be prepared to restart a cluster-wide SCAN from cursor '0'
- Do not persist cursor tokens across application restarts or share across client instances
- Process SCAN pages promptly to avoid cursor expiry
- Increase COUNT to reduce round trips and total scan duration
When it happens
Trigger: Passing a cursor from a previous SCAN that has expired, a cursor from a different client instance, or a cursor after the client reconnected and lost its cursor state.
Common situations: Long pause between SCAN pages exceeding cursor expiry; reusing a cursor from a previous application run; client reconnected (slots refreshed) and internal cursor store was cleared; load balancer routing requests to different client instances.
Related errors
- FT.CURSOR: the node serving cursor
- FT.CURSOR: unknown cursor
- SCAN: no master nodes available
- SCAN: node serving this cursor has left the cluster —…
- All replies must be array of numbers for logical AND…
AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11).
Data as JSON: /api/errors/621e33d293793041.
Report an issue: GitHub.
Appendix: source
Thrown at packages/client/lib/cluster/request-response-policies/scan-cursor.ts:46
*/
export const routeScan: RequestRouter = async (slots, parser) => {
// Malformed raw command (missing cursor): forward to any node so the server
// returns its own arity error instead of a client-side TypeError.
if (parser.redisArgs.length < 2) {
return [{ client: await slots.nodeClient(slots.getRandomNode()) }];
}
const cursorArg = argToString(parser.redisArgs[1]);
if (cursorArg === '0') {
const address = slots.nextScanTarget(EMPTY_VISITED);
if (!address) throw new Error('SCAN: no master nodes available');
return [{ client: await pinnedMaster(slots, address) }];
}
const entry = slots.lookupScanCursor(cursorArg);
if (!entry) {
throw new Error(
`SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN cursors are ` +
`minted per client instance and expire when idle — restart the scan from 0.`
);
}
return [{
client: await pinnedMaster(slots, entry.address),
parser: withCursor(parser, entry.cursor)
}];
};
const EMPTY_VISITED: ReadonlySet<string> = new Set();
async function pinnedMaster(slots: ClusterSlots, address: string) {
const client = await slots.getMasterByAddress(address);
if (!client) {
throw new Error(
`SCAN: node ${address} serving this cursor has left the cluster — ` +
`restart the scan from 0.`View on GitHub (pinned to 90fd0652bc)