redis/node-redis · error · Error

SCAN: node serving this cursor has left the cluster —…

Error message

SCAN: node ${address} serving this cursor has left the cluster — restart the scan from 0.

What it means

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.

Solutions

  1. Restart the scan from cursor '0'
  2. Use a larger COUNT to finish each node faster and reduce the window for topology changes
  3. For Sentinel deployments, use RedisSentinel.scanIterator which handles MASTER_CHANGE events with ScanIteratorInterruptedError
Defensive patterns

Strategy: retry

Try / catch

async function scanWithFailoverRetry(client, fn) {
  for (;;) {
    try {
      for await (const keys of client.scanIterator()) {
        await fn(keys);
      }
      return;
    } catch (e) {
      if (e.message.includes('has left the cluster')) {
        continue; // restart from 0
      }
      throw e;
    }
  }
}

Prevention

When it happens

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

Common situations: 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.

Related errors


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

Appendix: source

Thrown at packages/client/lib/cluster/request-response-policies/scan-cursor.ts:62

  const entry = slots.lookupScanCursor(cursorArg);
  if (!entry) {
    throw new Error(
      `SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN cursors are ` +
      `minted per client instance and expire when idle — restart the scan from 0.`
    );
  }
  return [{
    client: await pinnedMaster(slots, entry.address),
    parser: withCursor(parser, entry.cursor)
  }];
};

const EMPTY_VISITED: ReadonlySet<string> = new Set();

async function pinnedMaster(slots: ClusterSlots, address: string) {
  const client = await slots.getMasterByAddress(address);
  if (!client) {
    throw new Error(
      `SCAN: node ${address} serving this cursor has left the cluster — ` +
      `restart the scan from 0.`
    );
  }
  return client;
}

/** Copy of the SCAN parser with the cursor argument (index 1) replaced. */
function withCursor(parser: CommandParser, cursor: string): CommandParser {
  const sub = new BasicCommandParser();
  const { redisArgs } = parser;
  for (let i = 0; i < redisArgs.length; i++) {
    sub.push(i === 1 ? cursor : redisArgs[i] as RedisArgument);
  }
  return sub;
}

/**

View on GitHub (pinned to 90fd0652bc)