redis/node-redis · error · Error
SCAN: node ${address} serving this cursor has left the clust
Error message
SCAN: node ${address} serving this cursor has left the cluster — restart the scan from 0. What it means
Thrown by pinnedMaster when the cluster-wide scan has a valid cursor token (so the scan is in progress) but the specific node address bound to that token can no longer be resolved by getMasterByAddress — the shard that was serving this scan has left the known cluster topology (failover, removal, or resharding).
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 bb5beb5657)
Solutions
- Restart the cluster-wide scan from cursor 0 — the serving node is gone and its server-side cursor state was lost with it.
- If scans must tolerate topology churn, wrap the loop in a retry that restarts from 0 on this specific error and dedupe keys.
- Reduce the time span of a single scan (smaller COUNT, faster per-page processing) to shrink the window for failover.
- Schedule large scans during stable topology windows or against a replica-only view if acceptable.
Example fix
// before: assumes the serving node stays for the whole scan
do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0');
// after: restart from 0 if the serving node left mid-scan
let seen = new Set();
let cursor = '0';
do {
try { const r = await cluster.scan(cursor); cursor = r.cursor; r.keys.forEach(k => seen.add(k)); }
catch (e) { if (String(e).includes('has left the cluster')) { cursor = '0'; seen = new Set(); continue; } throw e; }
} while (cursor !== '0'); Defensive patterns
Strategy: retry
Validate before calling
// Cannot validate ahead of time — topology changes asynchronously. // Pattern: wrap the scan in a restart-on-failover loop.
Try / catch
try { await runScan(); } catch (e) { if (/has left the cluster/.test(e.message)) { seen.clear(); cursor = '0'; return runScan(); } throw e; } Prevention
- Treat any 'has left the cluster' error as a restart-from-zero signal.
- Dedup yielded keys across restarts since scans are not transactional.
- Shorten scan duration (smaller COUNT) to reduce failover exposure.
When it happens
Trigger: A Sentinel/cluster failover or reshard occurs between two scan pages: the node holding the scan's server-side cursor is demoted, removed, or its address no longer maps to a master in the current slots view, so getMasterByAddress returns null.
Common situations: Cluster topology changes during a long-running scan; a node crash and failover mid-iteration; slot migration that moves the scanned keyspace to a new node. These are inherent to any cluster-wide scan and the message tells you to restart.
Related errors
- FT.CURSOR: the node serving cursor ${token} on index "${argT
- SCAN: no master nodes available
- SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN curso
- Scan iteration was interrupted by a Sentinel master change
- Could not find shard
AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03).
Data as JSON: /data/errors/e6fb060e5e6b8819.json.
Report an issue: GitHub.