redis/redis-py · error · RedisClusterException
Cannot execute FT.CURSOR commands without FT.AGGREGATE
Error message
Cannot execute FT.CURSOR commands without FT.AGGREGATE
What it means
Raised by get_special_nodes() in the async cluster client when an FT.CURSOR command is dispatched but self._aggregate_nodes is empty. The cluster client only populates _aggregate_nodes as a side effect of running FT.AGGREGATE (cluster.py:979), so it knows which shard produced the cursor. Without that prior call there is no node to send the cursor read to, so the library refuses rather than guessing.
Solutions
- Run FT.AGGREGATE on the same RedisCluster client first, then immediately issue FT.CURSOR on the returned cursor id.
- Keep the aggregate and cursor calls on the same client instance; do not reinitialize the cluster between them.
- If the cluster reinitialized (MOVED storm / resharding), re-issue the FT.AGGREGATE to obtain a fresh cursor and node set.
- Avoid persisting a cursor id across process/client restarts; cursors are tied to a specific shard and the client's cached aggregate nodes.
Example fix
// before
rc = RedisCluster(host='localhost', port=7000)
await rc.execute_command('FT.CURSOR', 'idx', '0', 'COUNT', 10)
// after
rc = RedisCluster(host='localhost', port=7000)
await rc.execute_command('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR', 'COUNT', 10)
await rc.execute_command('FT.CURSOR', 'idx', '<cursor_id>', 'COUNT', 10) Defensive patterns
Strategy: validation
Validate before calling
# Before issuing FT.CURSOR, confirm the client has aggregate nodes cached
def assert_aggregate_ready(rc):
if not getattr(rc, '_aggregate_nodes', None):
raise RuntimeError('Run FT.AGGREGATE on this client before FT.CURSOR')
await assert_aggregate_ready(rc)
await rc.execute_command('FT.CURSOR', 'idx', cursor_id, 'COUNT', 10) Type guard
def has_aggregate_nodes(rc) -> bool:
return bool(getattr(rc, '_aggregate_nodes', None)) Try / catch
from redis.exceptions import RedisClusterException
try:
await rc.execute_command('FT.CURSOR', 'idx', cursor_id)
except RedisClusterException as e:
if 'FT.CURSOR commands without FT.AGGREGATE' in str(e):
await rc.execute_command('FT.AGGREGATE', 'idx', '*', 'WITHCURSOR')
# then retry the cursor read Prevention
- Always pair FT.CURSOR with a preceding FT.AGGREGATE on the same client instance.
- Do not reuse cursor ids after reinitialize(); re-run the aggregate to get fresh nodes + cursor.
- Treat cursors as short-lived session state bound to one cluster client.
When it happens
Trigger: Calling FT.CURSOR (e.g. client.ft().cursor_read(...) / cursor_delete) on a RedisCluster client before any FT.AGGREGATE has been executed against it in the current slots-cache lifetime, or on a freshly initialized client where the cached aggregate nodes were cleared by a reinitialize() call.
Common situations: Using redis-py's search module with a RedisCluster handle and resuming an aggregate cursor across two separate client instances or after a cluster topology refresh wiped _aggregate_nodes. Also from manually executing execute_command('FT.CURSOR', ...) without a preceding FT.AGGREGATE.
Related errors
- At least a command with a key is needed to identify a node
- Cannot identify slot number for command
- command doesn't exist in Redis commands
- Invalid args in command
- No targets were found to execute
AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10).
Data as JSON: /api/errors/01d29d4eafbf3b5e.
Report an issue: GitHub.
Appendix: source
Thrown at redis/asyncio/cluster.py:890
async def get_nodes_from_slot(self, command: str, *args):
"""
Returns a list of nodes that hold the specified keys' slots.
"""
# get the node that holds the key's slot
return [
self.nodes_manager.get_node_from_slot(
await self._determine_slot(command, *args),
self.read_from_replicas and command in READ_COMMANDS,
self.load_balancing_strategy if command in READ_COMMANDS else None,
)
]
def get_special_nodes(self) -> Optional[list["ClusterNode"]]:
"""
Returns a list of nodes for commands with a special policy.
"""
if not self._aggregate_nodes:
raise RedisClusterException(
"Cannot execute FT.CURSOR commands without FT.AGGREGATE"
)
return self._aggregate_nodes
def keyslot(self, key: EncodableT) -> int:
"""
Find the keyslot for a given key.
See: https://redis.io/docs/manual/scaling/#redis-cluster-data-sharding
"""
return key_slot(self.encoder.encode(key))
# HIMPORT orchestration (async mirror of redis.cluster.RedisCluster). The one
# shared HImportRegistry is mutated once by PREPARE/DISCARD/DISCARDALL and applied
# lazily per node; SET routes by key slot to the owning primary's ClusterNode.
# See ``.agents/himport_client_support_spec.md``.
View on GitHub (pinned to 6a6b581b48)