{"id":"e6fb060e5e6b8819","repo":"redis/node-redis","slug":"scan-node-address-serving-this-cursor-has-left","errorCode":null,"errorMessage":"SCAN: node ${address} serving this cursor has left the cluster — restart the scan from 0.","messagePattern":"SCAN: node (.+?) serving this cursor has left the cluster — restart the scan from 0\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/client/lib/cluster/request-response-policies/scan-cursor.ts","lineNumber":62,"sourceCode":"  const entry = slots.lookupScanCursor(cursorArg);\n  if (!entry) {\n    throw new Error(\n      `SCAN: unknown cursor \"${cursorArg}\". Cluster-wide SCAN cursors are ` +\n      `minted per client instance and expire when idle — restart the scan from 0.`\n    );\n  }\n  return [{\n    client: await pinnedMaster(slots, entry.address),\n    parser: withCursor(parser, entry.cursor)\n  }];\n};\n\nconst EMPTY_VISITED: ReadonlySet<string> = new Set();\n\nasync function pinnedMaster(slots: ClusterSlots, address: string) {\n  const client = await slots.getMasterByAddress(address);\n  if (!client) {\n    throw new Error(\n      `SCAN: node ${address} serving this cursor has left the cluster — ` +\n      `restart the scan from 0.`\n    );\n  }\n  return client;\n}\n\n/** Copy of the SCAN parser with the cursor argument (index 1) replaced. */\nfunction withCursor(parser: CommandParser, cursor: string): CommandParser {\n  const sub = new BasicCommandParser();\n  const { redisArgs } = parser;\n  for (let i = 0; i < redisArgs.length; i++) {\n    sub.push(i === 1 ? cursor : redisArgs[i] as RedisArgument);\n  }\n  return sub;\n}\n\n/**","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/redis/node-redis/blob/bb5beb56578573910e2ee8f39681edc214c41398/packages/client/lib/cluster/request-response-policies/scan-cursor.ts#L44-L80","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: assumes the serving node stays for the whole scan\ndo { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0');\n\n// after: restart from 0 if the serving node left mid-scan\nlet seen = new Set();\nlet cursor = '0';\ndo {\n  try { const r = await cluster.scan(cursor); cursor = r.cursor; r.keys.forEach(k => seen.add(k)); }\n  catch (e) { if (String(e).includes('has left the cluster')) { cursor = '0'; seen = new Set(); continue; } throw e; }\n} while (cursor !== '0');","handlingStrategy":"retry","validationCode":"// Cannot validate ahead of time — topology changes asynchronously.\n// Pattern: wrap the scan in a restart-on-failover loop.","typeGuard":null,"tryCatchPattern":"try { await runScan(); } catch (e) { if (/has left the cluster/.test(e.message)) { seen.clear(); cursor = '0'; return runScan(); } throw e; }","preventionTips":["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."],"tags":["cluster","scan","failover","topology","cursor"],"analyzedSha":"bb5beb56578573910e2ee8f39681edc214c41398","analyzedAt":"2026-08-03T19:09:15.686Z","schemaVersion":2}