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

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.

Solutions

  1. Catch ScanIteratorInterruptedError and restart the scan from the beginning
  2. Use a larger COUNT to complete scans faster and narrow the failover window
  3. Track processed keys externally if resumable semantics are needed (SCAN has no server-side resume)

Example fix

// before
for await (const keys of sentinel.scanIterator()) {
  console.log(keys);
}

// after
let done = false;
while (!done) {
  try {
    for await (const keys of sentinel.scanIterator()) {
      console.log(keys);
    }
    done = true;
  } catch (e) {
    if (e instanceof ScanIteratorInterruptedError) {
      console.warn('Scan interrupted by master change, restarting...');
      continue;
    }
    throw e;
  }
}
Defensive patterns

Strategy: try-catch

Type guard

import { ScanIteratorInterruptedError } from 'redis';

function isScanInterrupted(e: unknown): e is ScanIteratorInterruptedError {
  return e instanceof ScanIteratorInterruptedError;
}

Try / catch

import { ScanIteratorInterruptedError } from 'redis';

for (;;) {
  try {
    for await (const keys of sentinel.scanIterator()) {
      process(keys);
    }
    break;
  } catch (e) {
    if (e instanceof ScanIteratorInterruptedError) {
      console.warn('Scan interrupted by master change, restarting...');
      continue;
    }
    throw e;
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: Failover during a long-running full-keyspace scan; planned maintenance or rolling restarts while iterating; Sentinel promoting a replica mid-scan.

Related errors


AI-assisted analysis of redis/node-redis@90fd0652bc (2026-08-11). Data as JSON: /api/errors/4235c1c5ca0abc2a. Report an issue: GitHub.

Appendix: 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 90fd0652bc)