redis/node-redis · warning · ScanIteratorInterruptedError

Scan iteration was interrupted by a Sentinel master change

Error message

Scan iteration was interrupted by a Sentinel master change

What it means

The first of three throw sites in RedisSentinel.scanIterator: at the top of each loop iteration it checks the masterChanged flag set by the 'topology-change' MASTER_CHANGE listener. If a failover was observed between yielding one page and starting the next SCAN, it throws ScanIteratorInterruptedError because the cursor from the old master no longer applies.

Source

Thrown at packages/client/lib/sentinel/index.ts:723

   * @throws {ScanIteratorInterruptedError} On observed `MASTER_CHANGE`.
   */
  async *scanIterator(
    this: RedisSentinelType<M, F, S, RESP, TYPE_MAPPING>,
    options?: ScanOptions & ScanIteratorOptions
  ) {
    let cursor: RedisArgument = options?.cursor ?? '0';
    let masterChanged = false;

    const handleTopologyChange = (event: RedisSentinelEvent) => {
      if (event.type === 'MASTER_CHANGE') {
        masterChanged = true;
      }
    };
    this.on('topology-change', handleTopologyChange);

    try {
      do {
        if (masterChanged) throw new ScanIteratorInterruptedError();

        // Route through _execute so reserveClient:true reuses the reserved
        // lease (instead of waiting forever on an empty master pool), and the
        // lease is released before yielding — consumers can issue other
        // commands inside the for-await loop without exhausting the pool.
        let reply;
        try {
          reply = await this._execute(
            false,
            client => {
              // Re-check after the lease resolves: a failover may have landed
              // while waiting on an empty master pool, in which case the lease
              // now points to a fresh client on the new master and SCAN would
              // resume with a cursor from the old master.
              if (masterChanged) throw new ScanIteratorInterruptedError();
              return (client as RedisClientType<RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping>).scan(cursor, options);
            }
          );

View on GitHub (pinned to bb5beb5657)

Solutions

  1. Catch ScanIteratorInterruptedError and restart the scan from the beginning (fresh scanIterator call).
  2. Run large scans outside failover windows or with idempotent dedup of keys.
  3. Reduce per-page processing time to shrink the window in which a failover can interrupt.
  4. Use the iterator's options.cursor only within a single uninterrupted run.

Example fix

// before
for await (const keys of sentinel.scanIterator()) { process(keys); } // throws mid-scan

// after
async function fullScan() {
  for (;;) {
    try { for await (const keys of sentinel.scanIterator()) { process(keys); } return; }
    catch (e) { if (e instanceof ScanIteratorInterruptedError) continue; throw e; }
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Cannot pre-validate an asynchronous failover. Restart-on-interrupt:
async function robustScan(sentinel, process) { for (;;) { try { for await (const k of sentinel.scanIterator()) process(k); return; } catch (e) { if (!(e instanceof ScanIteratorInterruptedError)) throw e; } } }

Type guard

function isScanInterrupted(e) { return e instanceof Error && /interrupted by a Sentinel master change/.test(e.message); }

Try / catch

try { for await (const keys of sentinel.scanIterator()) process(keys); } catch (e) { if (isScanInterrupted(e)) { /* restart from new scanIterator */ } else throw e; }

Prevention

When it happens

Trigger: Iterating for await (const keys of sentinel.scanIterator()) and a Sentinel master failover event arrives between pages; the next iteration's pre-check sees masterChanged=true and aborts before issuing SCAN.

Common situations: Long-running full-key scans over a Sentinel-managed topology that fails over (maintenance, crash, manual failover) mid-iteration; scans running during a scheduled failover window.

Related errors


AI-assisted analysis of redis/node-redis@bb5beb5657 (2026-08-03). Data as JSON: /data/errors/4235c1c5ca0abc2a.json. Report an issue: GitHub.