redis/redis-py · error · RedisClusterException

At least a command with a key is needed to identify a node

Error message

At least a command with a key is needed to identify a node

What it means

Raised by TransactionStrategy._get_client_and_connection_for_transaction (redis/cluster.py:4648) when _pipeline_slots is empty. A cluster transaction must pin to a single slot to choose its connection; if no keyed command has established a slot yet (only slot-agnostic commands queued), the library cannot pick a node and aborts.

Solutions

  1. Ensure at least one keyed command (GET/SET/etc.) is queued before execute() so a slot is fixed.
  2. If you only need keyless commands, use a non-transactional pipeline or execute directly on a node.
  3. For WATCH, watch a key first so the slot is established before MULTI.

Example fix

// before
with rc.pipeline(transaction=True) as pipe:
    pipe.ping()
    pipe.execute()
// after
with rc.pipeline(transaction=True) as pipe:
    pipe.set('anchor:{tag}', '1')
    pipe.ping()
    pipe.execute()
Defensive patterns

Strategy: validation

Validate before calling

with rc.pipeline(transaction=True) as pipe:
    if not any(cmd_has_key(c) for c in planned_commands):
        pipe.set('anchor:{tag}', '1')  # ensure a slot is fixed
    for c in planned_commands:
        pipe.execute_command(*c)
    pipe.execute()

Type guard

def has_keyed_command(commands) -> bool:
    keyless = {'PING','INFO','DBSIZE','FLUSHALL','FLUSHDB'}
    return any(c[0].upper() not in keyless for c in commands)

Try / catch

from redis.exceptions import RedisClusterException
try:
    pipe.execute()
except RedisClusterException as e:
    if 'At least a command with a key' in str(e):
        # queue a keyed command and retry, or use a non-transactional pipeline
        ...

Prevention

When it happens

Trigger: Opening a transactional pipeline and calling execute() having queued only keyless/slot-agnostic commands (e.g. only PING, INFO, or zero-key EVAL), or calling watch()/immediate commands before any key has fixed a slot.

Common situations: Building a transaction dynamically and forgetting to include a keyed command; executing an empty-ish transaction for health checks; race where the first keyed command was skipped due to a condition.

Related errors


AI-assisted analysis of redis/redis-py@6a6b581b48 (2026-08-10). Data as JSON: /api/errors/9c9d1eb1497eea1d. Report an issue: GitHub.

Appendix: source

Thrown at redis/cluster.py:4648

            and not self._transaction_has_keyed_slot
        ):
            # Prior slots came only from zero-key scripts; retarget.
            self._pipeline_slots.clear()
        if slot_number is not None:
            self._transaction_has_keyed_slot = True
        return slot_number

    def _get_client_and_connection_for_transaction(self) -> Tuple[Redis, Connection]:
        """
        Find a connection for a pipeline transaction.

        For running an atomic transaction, watch keys ensure that contents have not been
        altered as long as the watch commands for those keys were sent over the same
        connection. So once we start watching a key, we fetch a connection to the
        node that owns that slot and reuse it.
        """
        if not self._pipeline_slots:
            raise RedisClusterException(
                "At least a command with a key is needed to identify a node"
            )

        node: ClusterNode = self._nodes_manager.get_node_from_slot(
            list(self._pipeline_slots)[0], False
        )
        redis_node: Redis = self._pipe.get_redis_connection(node)
        if self._transaction_connection:
            if not redis_node.connection_pool.owns_connection(
                self._transaction_connection
            ):
                previous_node = self._nodes_manager.find_connection_owner(
                    self._transaction_connection
                )
                previous_node.connection_pool.release(self._transaction_connection)
                self._transaction_connection = None

        if not self._transaction_connection:

View on GitHub (pinned to 6a6b581b48)