redis/node-redis · error · Error
SCAN: node serving this cursor has left the cluster —…
Error message
SCAN: node ${address} serving this cursor has left the cluster — restart the scan from 0. What it means
The master node bound to an in-progress cluster-wide SCAN cursor is no longer in the cluster topology. getMasterByAddress returned null, meaning the node departed during a failover or reshard. The cursor's server-side state is on a node that no longer exists, so the scan cannot continue.
Solutions
- Restart the scan from cursor '0'
- Use a larger COUNT to finish each node faster and reduce the window for topology changes
- For Sentinel deployments, use RedisSentinel.scanIterator which handles MASTER_CHANGE events with ScanIteratorInterruptedError
Defensive patterns
Strategy: retry
Try / catch
async function scanWithFailoverRetry(client, fn) {
for (;;) {
try {
for await (const keys of client.scanIterator()) {
await fn(keys);
}
return;
} catch (e) {
if (e.message.includes('has left the cluster')) {
continue; // restart from 0
}
throw e;
}
}
} Prevention
- Be prepared to restart cluster-wide scans from cursor '0' on topology changes
- Use a larger COUNT to finish faster and reduce the failover exposure window
- Avoid long-running scans during planned cluster maintenance
When it happens
Trigger: A mid-scan topology change where the node holding the cursor's real server-side SCAN state was removed from the cluster (failover, node removal, or reshard).
Common situations: Failover during a long-running cluster-wide scan; node removed during cluster maintenance or scaling; network partition causing the client to lose track of a node.
Related errors
- Cannot find node
- SCAN: no master nodes available
- Could not find shard
- FT.CURSOR: the node serving cursor
- no valid master node
AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11).
Data as JSON: /api/errors/e6fb060e5e6b8819.
Report an issue: GitHub.
Appendix: source
Thrown at packages/client/lib/cluster/request-response-policies/scan-cursor.ts:62
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.`
);
}
return client;
}
/** Copy of the SCAN parser with the cursor argument (index 1) replaced. */
function withCursor(parser: CommandParser, cursor: string): CommandParser {
const sub = new BasicCommandParser();
const { redisArgs } = parser;
for (let i = 0; i < redisArgs.length; i++) {
sub.push(i === 1 ? cursor : redisArgs[i] as RedisArgument);
}
return sub;
}
/**View on GitHub (pinned to 90fd0652bc)