redis/node-redis · error · Error

SCAN: node ${address} serving this cursor has left the clust

Error message

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

What it means

Thrown by pinnedMaster when the cluster-wide scan has a valid cursor token (so the scan is in progress) but the specific node address bound to that token can no longer be resolved by getMasterByAddress — the shard that was serving this scan has left the known cluster topology (failover, removal, or resharding).

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 bb5beb5657)

Solutions

  1. Restart the cluster-wide scan from cursor 0 — the serving node is gone and its server-side cursor state was lost with it.
  2. If scans must tolerate topology churn, wrap the loop in a retry that restarts from 0 on this specific error and dedupe keys.
  3. Reduce the time span of a single scan (smaller COUNT, faster per-page processing) to shrink the window for failover.
  4. Schedule large scans during stable topology windows or against a replica-only view if acceptable.

Example fix

// before: assumes the serving node stays for the whole scan
do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0');

// after: restart from 0 if the serving node left mid-scan
let seen = new Set();
let cursor = '0';
do {
  try { const r = await cluster.scan(cursor); cursor = r.cursor; r.keys.forEach(k => seen.add(k)); }
  catch (e) { if (String(e).includes('has left the cluster')) { cursor = '0'; seen = new Set(); continue; } throw e; }
} while (cursor !== '0');
Defensive patterns

Strategy: retry

Validate before calling

// Cannot validate ahead of time — topology changes asynchronously.
// Pattern: wrap the scan in a restart-on-failover loop.

Try / catch

try { await runScan(); } catch (e) { if (/has left the cluster/.test(e.message)) { seen.clear(); cursor = '0'; return runScan(); } throw e; }

Prevention

When it happens

Trigger: A Sentinel/cluster failover or reshard occurs between two scan pages: the node holding the scan's server-side cursor is demoted, removed, or its address no longer maps to a master in the current slots view, so getMasterByAddress returns null.

Common situations: Cluster topology changes during a long-running scan; a node crash and failover mid-iteration; slot migration that moves the scanned keyspace to a new node. These are inherent to any cluster-wide scan and the message tells you to restart.

Related errors


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