{"record":{"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/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/cluster/request-response-policies/scan-cursor.ts#L44-L80","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function scanWithFailoverRetry(client, fn) {\n  for (;;) {\n    try {\n      for await (const keys of client.scanIterator()) {\n        await fn(keys);\n      }\n      return;\n    } catch (e) {\n      if (e.message.includes('has left the cluster')) {\n        continue; // restart from 0\n      }\n      throw e;\n    }\n  }\n}","preventionTips":["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"],"tags":["cluster","scan","topology","failover"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}