redis/node-redis · critical · Error

Request policy produced no target nodes

Error message

Request policy ${requestPolicy} produced no target nodes

What it means

Thrown when a cluster command's request-policy router returns an empty execution plan — zero target nodes were selected. This occurs for all_nodes/all_shards policies (e.g. DBSIZE, WAIT, SCRIPT EXISTS, FLUSHALL) when the cluster's slot table contains no known master nodes, meaning topology discovery has not completed or every node is unreachable.

Solutions

  1. Ensure `await cluster.connect()` completes before issuing any commands
  2. Verify all configured rootNodes are reachable and healthy
  3. Implement reconnection logic with exponential backoff and retry the command after slots are re-discovered
  4. Check for RootNodesUnavailableError during connect() — if it fires, the cluster never established a topology

Example fix

// before
const cluster = createCluster({ rootNodes: ['redis://node-1:6379'] });
await cluster.dbSize(); // throws: produced no target nodes

// after
const cluster = createCluster({ rootNodes: ['redis://node-1:6379'] });
await cluster.connect();
await cluster.dbSize(); // works
Defensive patterns

Strategy: validation

Validate before calling

// Check that the cluster has discovered nodes before issuing
// all_shards / all_nodes commands.
import { commandExists } from './cluster-health';

async function assertClusterReady(cluster: RedisClusterType) {
  const nodes = cluster.nodes('master');
  if (nodes.length === 0) {
    throw new Error('Cluster has no master nodes — call await cluster.connect() first');
  }
}

// call before all_shards/all_nodes commands
await assertClusterReady(cluster);
await cluster.dbSize();

Try / catch

try {
  await cluster.dbSize();
} catch (err) {
  if (err instanceof Error && err.message.includes('produced no target nodes')) {
    // topology not yet established — reconnect and retry
    await cluster.connect();
    await cluster.dbSize();
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: Calling an all_shards or all_nodes command (DBSIZE, WAIT, WAITAOF, SCRIPT EXISTS, SCRIPT LOAD, MEMORY PURGE, CLIENT NO-EVICT) before cluster.connect() resolves, or when all seed nodes are down and slot discovery has failed. The routers routeAllNodes/routeAllShards call slots.getAllNodes()/getAllMasterNodes() which return empty arrays when the topology is empty.

Common situations: Application starts issuing commands before awaiting connect(); cluster-wide outage or network partition where all rootNodes are unreachable; race condition during failover where the slot table is temporarily empty after rediscovery.

Related errors


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

Appendix: source

Thrown at packages/client/lib/cluster/index.ts:589

    }
    // Validated before any per-node promise is dispatched — throwing after
    // would orphan in-flight rejections (unhandled-rejection noise) and run
    // side effects for a reply that can never be reduced.
    const reducer = RESPONSE_REDUCERS[responsePolicy];
    if (!reducer) {
      throw new Error(`Unknown response policy ${responsePolicy}`);
    }
    // Routers are typed against the erased base cluster types (routing is
    // below the typed command surface); bridge this instantiation's slots in.
    const plan = await router(
      this._slots as unknown as Parameters<typeof router>[0],
      parser,
      readonly,
      policy.keySpecs
    );

    if (plan.length === 0) {
      throw new Error(`Request policy ${requestPolicy} produced no target nodes`);
    }

    // Numeric aggregation must see raw numbers: strip the caller's type
    // mapping from the per-node executions (a `NUMBER: String` mapping would
    // feed strings into the numeric reducers) and re-apply it to the
    // aggregated result below. Pass-through policies keep the mapping — their
    // replies reach the caller undisturbed.
    const numericAgg = NUMERIC_AGG_POLICIES.has(responsePolicy);
    const requestedMapping = options?.typeMapping;
    const execOptions = numericAgg && requestedMapping
      ? { ...options, typeMapping: undefined }
      : options;

    // Track the actually-serving client for single-target plans: after a
    // MOVED/ASK redirect the reply comes from a different node than
    // `plan[0].client`, and the post-reply hooks must bind cursors to the node
    // that really served. Multi-target hooks are no-ops, so nothing to track.
    const served: { client?: RedisClientType<M, F, S, RESP, TYPE_MAPPING> } = {};

View on GitHub (pinned to 90fd0652bc)