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
- Ensure `await cluster.connect()` completes before issuing any commands
- Verify all configured rootNodes are reachable and healthy
- Implement reconnection logic with exponential backoff and retry the command after slots are re-discovered
- 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
- Always await cluster.connect() before issuing any commands
- Implement a health-check wrapper that verifies node availability before all_shards/all_nodes commands
- Use exponential backoff reconnection logic for transient cluster unavailability
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
- Cannot find node
- Could not find shard
- [Proxy] No destination node
- SCAN: no master nodes available
- SCAN: node serving this cursor has left the cluster —…
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)