redis/node-redis · error · Error

SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN curso

Error message

SCAN: unknown cursor "${cursorArg}". Cluster-wide SCAN cursors are minted per client instance and expire when idle — restart the scan from 0.

What it means

Thrown when the cursor token passed to cluster SCAN does not match any cursor this client instance minted. Cluster-wide SCAN swaps each server cursor for an opaque per-instance token that maps back to (node, real cursor, visited set); tokens expire when idle and never survive across client instances or a restart. The token is opaque on purpose — only the same instance that minted it can resolve it.

Source

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

 */
export const routeScan: RequestRouter = async (slots, parser) => {
  // Malformed raw command (missing cursor): forward to any node so the server
  // returns its own arity error instead of a client-side TypeError.
  if (parser.redisArgs.length < 2) {
    return [{ client: await slots.nodeClient(slots.getRandomNode()) }];
  }

  const cursorArg = argToString(parser.redisArgs[1]);

  if (cursorArg === '0') {
    const address = slots.nextScanTarget(EMPTY_VISITED);
    if (!address) throw new Error('SCAN: no master nodes available');
    return [{ client: await pinnedMaster(slots, address) }];
  }

  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.`

View on GitHub (pinned to bb5beb5657)

Solutions

  1. Restart the cluster-wide scan from cursor 0 — the error message itself directs this.
  2. Keep scan pages flowing without long gaps so the internal cursor token is not evicted.
  3. Do not persist, share, or transfer cluster SCAN cursor tokens across client instances or processes.
  4. Run the full scan loop within a single client lifetime.

Example fix

// before: resuming a stale cursor token from a previous run
let cursor = loadFromDb('scanCursor');
do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0');

// after: always start a cluster-wide scan at 0
do { const r = await cluster.scan(cursor); cursor = r.cursor; /* process promptly */ } while (cursor !== '0');
Defensive patterns

Strategy: validation

Validate before calling

// Cluster SCAN cursors are opaque per-instance tokens — never persist or reuse.
// Only validation: start at '0' and do not pass a foreign/stale token.
function startCursor(stored) { return stored === '0' ? '0' : '0'; } // always restart

Type guard

function isFreshClusterScanStart(c) { return c === '0'; }

Try / catch

try { do { const r = await cluster.scan(cursor); cursor = r.cursor; } while (cursor !== '0'); } catch (e) { if (/unknown cursor/.test(e.message)) { cursor = '0'; /* restart, dedupe keys */ continue; } throw e; }

Prevention

When it happens

Trigger: Reusing a cursor string from a previous, completed, or expired scan iteration; copying a cursor between two cluster client instances; persisting a cursor to disk and reloading it later; the client's internal cursor map evicted the entry after long idleness between scan pages.

Common situations: Long pauses between scan pages (e.g. heavy per-key processing) that exceed the idle eviction window; serializing a scan cursor into a job queue and resuming on a different process/client; splitting a scan loop across reconnects where the client object was recreated.

Related errors


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