{"record":{"id":"4235c1c5ca0abc2a","repo":"redis/node-redis","slug":"scan-iteration-was-interrupted-by-a-sentinel-maste","errorCode":null,"errorMessage":"Scan iteration was interrupted by a Sentinel master change","messagePattern":"Scan iteration was interrupted by a Sentinel master change","errorType":"exception","errorClass":"ScanIteratorInterruptedError","httpStatus":null,"severity":"warning","filePath":"packages/client/lib/sentinel/index.ts","lineNumber":723,"sourceCode":"   * @throws {ScanIteratorInterruptedError} On observed `MASTER_CHANGE`.\n   */\n  async *scanIterator(\n    this: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>,\n    options?: ScanOptions & ScanIteratorOptions\n  ) {\n    let cursor: RedisArgument = options?.cursor ?? '0';\n    let masterChanged = false;\n\n    const handleTopologyChange = (event: RedisSentinelEvent) => {\n      if (event.type === 'MASTER_CHANGE') {\n        masterChanged = true;\n      }\n    };\n    this.on('topology-change', handleTopologyChange);\n\n    try {\n      do {\n        if (masterChanged) throw new ScanIteratorInterruptedError();\n\n        // Route through _execute so reserveClient:true reuses the reserved\n        // lease (instead of waiting forever on an empty master pool), and the\n        // lease is released before yielding — consumers can issue other\n        // commands inside the for-await loop without exhausting the pool.\n        let reply;\n        try {\n          reply = await this._execute(\n            false,\n            client => {\n              // Re-check after the lease resolves: a failover may have landed\n              // while waiting on an empty master pool, in which case the lease\n              // now points to a fresh client on the new master and SCAN would\n              // resume with a cursor from the old master.\n              if (masterChanged) throw new ScanIteratorInterruptedError();\n              return (client as RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>).scan(cursor, options);\n            }\n          );","sourceCodeStart":705,"sourceCodeEnd":741,"githubUrl":"https://github.com/redis/node-redis/blob/90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58/packages/client/lib/sentinel/index.ts#L705-L741","documentation":"At the top of each scanIterator loop iteration, the code checks whether a MASTER_CHANGE topology event was observed since the last page. SCAN cursors are per-node server-side state, so a failover invalidates the cursor — the iterator aborts with ScanIteratorInterruptedError rather than silently returning stale or duplicated results.","triggerScenarios":"A Sentinel failover (MASTER_CHANGE event) occurs between yielding one SCAN page and requesting the next. The topology-change listener sets masterChanged=true and the top-of-loop check fires on the next iteration.","commonSituations":"Failover during a long-running full-keyspace scan; planned maintenance or rolling restarts while iterating; Sentinel promoting a replica mid-scan.","solutions":["Catch ScanIteratorInterruptedError and restart the scan from the beginning","Use a larger COUNT to complete scans faster and narrow the failover window","Track processed keys externally if resumable semantics are needed (SCAN has no server-side resume)"],"exampleFix":"// before\nfor await (const keys of sentinel.scanIterator()) {\n  console.log(keys);\n}\n\n// after\nlet done = false;\nwhile (!done) {\n  try {\n    for await (const keys of sentinel.scanIterator()) {\n      console.log(keys);\n    }\n    done = true;\n  } catch (e) {\n    if (e instanceof ScanIteratorInterruptedError) {\n      console.warn('Scan interrupted by master change, restarting...');\n      continue;\n    }\n    throw e;\n  }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"import { ScanIteratorInterruptedError } from 'redis';\n\nfunction isScanInterrupted(e: unknown): e is ScanIteratorInterruptedError {\n  return e instanceof ScanIteratorInterruptedError;\n}","tryCatchPattern":"import { ScanIteratorInterruptedError } from 'redis';\n\nfor (;;) {\n  try {\n    for await (const keys of sentinel.scanIterator()) {\n      process(keys);\n    }\n    break;\n  } catch (e) {\n    if (e instanceof ScanIteratorInterruptedError) {\n      console.warn('Scan interrupted by master change, restarting...');\n      continue;\n    }\n    throw e;\n  }\n}","preventionTips":["Always wrap scanIterator in a retry loop that catches ScanIteratorInterruptedError","Use a larger COUNT to reduce the total scan duration and failover exposure","Avoid scanning during planned Sentinel maintenance or rolling restarts","Log interruptions to monitor failover frequency in your environment"],"tags":["sentinel","scan","failover","iterator"],"backgroundTag":null,"analyzedSha":"90fd0652bc3f2a0a1b2f79fa9096b02a86b0ac58","analyzedAt":"2026-08-11T15:37:21.243Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}