redis/node-redis · error · Error
SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN curso
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
Thrown when the cursor token passed to cluster SCAN does not match any cursor this client instance minted. Cluster-wide SCAN swaps each server cursor for an opaque per-instance token that maps back to (node, real cursor, visited set); tokens expire when idle and never survive across client instances or a restart. The token is opaque on purpose — only the same instance that minted it can resolve it.
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 bb5beb5657)
Solutions
- Restart the cluster-wide scan from cursor 0 — the error message itself directs this.
- Keep scan pages flowing without long gaps so the internal cursor token is not evicted.
- Do not persist, share, or transfer cluster SCAN cursor tokens across client instances or processes.
- Run the full scan loop within a single client lifetime.
Example fix
// before: resuming a stale cursor token from a previous run
let cursor = loadFromDb('scanCursor');
do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0');
// after: always start a cluster-wide scan at 0
do { const r = await cluster.scan(cursor); cursor = r.cursor; /* process promptly */ } while (cursor !== '0'); Defensive patterns
Strategy: validation
Validate before calling
// Cluster SCAN cursors are opaque per-instance tokens — never persist or reuse.
// Only validation: start at '0' and do not pass a foreign/stale token.
function startCursor(stored) { return stored === '0' ? '0' : '0'; } // always restart Type guard
function isFreshClusterScanStart(c) { return c === '0'; } Try / catch
try { do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0'); } catch (e) { if (/unknown cursor/.test(e.message)) { cursor = '0'; /* restart, dedupe keys */ continue; } throw e; } Prevention
- Never persist, serialize, or share cluster SCAN cursor tokens.
- Keep scan pages flowing without long idle gaps.
- Run a complete scan within a single client instance lifetime.
When it happens
Trigger: Reusing a cursor string from a previous, completed, or expired scan iteration; copying a cursor between two cluster client instances; persisting a cursor to disk and reloading it later; the client's internal cursor map evicted the entry after long idleness between scan pages.
Common situations: Long pauses between scan pages (e.g. heavy per-key processing) that exceed the idle eviction window; serializing a scan cursor into a job queue and resuming on a different process/client; splitting a scan loop across reconnects where the client object was recreated.
Related errors
- SCAN: node ${address} serving this cursor has left the clust
- SCAN: no master nodes available
- topologyRefreshOnReconnectionAttempt must be undefined, fals
- topologyRefreshOnReconnectionAttempt should return `false |
- Client Side Caching is only supported with RESP3
AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03).
Data as JSON: /data/errors/621e33d293793041.json.
Report an issue: GitHub.