redis/redis-rb · error · Redis::Distributed::CannotDistribute

PIPELINED cannot be used in Redis::Distributed because the k

Error message

PIPELINED cannot be used in Redis::Distributed because the keys involved need to be on the same server or because we cannot guarantee that the operation will be atomic.

What it means

Redis::Distributed routes every command individually through the client-side hash ring, so there is no single connection to batch commands onto — a pipeline on the facade would interleave commands destined for different servers. #pipelined therefore raises CannotDistribute unconditionally. Pipelining itself is not forbidden in a sharded app: each underlying node is a normal Redis client and pipelines fine; only the facade-level block is rejected. Redis::Cluster, by contrast, supports pipelined because redis-cluster-client splits the batch per node.

Source

Thrown at lib/redis/distributed.rb:1325

          node.watch(*keys, &block)
        rescue StandardError
          @watch_key = nil
          raise
        end
      end
    end

    # Forget about all watched keys.
    def unwatch
      raise CannotDistribute, :unwatch unless @watch_key

      result = node_for(@watch_key).unwatch
      @watch_key = nil
      result
    end

    def pipelined
      raise CannotDistribute, :pipelined
    end

    # Mark the start of a transaction block.
    def multi(&block)
      raise CannotDistribute, :multi unless @watch_key

      node_for(@watch_key).multi(&block)
    end

    # Execute all commands issued after MULTI.
    def exec
      raise CannotDistribute, :exec unless @watch_key

      result = node_for(@watch_key).exec
      @watch_key = nil
      result
    end

View on GitHub (pinned to 2ba9010b91)

Solutions

  1. Drop the pipelined block and issue the commands singly through the facade — correct everywhere, one round trip per command
  2. Group your operations by destination node and pipeline on each node: dist.nodes.each { |n| n.pipelined { |p| ... } } — or route by key with dist.node_for(key).pipelined
  3. If the batch targets known keys, build per-node buckets first (key -> dist.node_for(key)), then run one pipeline per bucket
  4. If pipelining at scale is a hard requirement, switch topology to Redis::Cluster, whose client fans a pipeline out per slot

Example fix

# before
dist.pipelined do |p|
  p.set("k1", "a")
  p.incr("k2")
end
# => Redis::Distributed::CannotDistribute

# after: group commands by node, one pipeline per node
commands = [[:set, "k1", "a"], [:incr, "k2"]]
commands.group_by { |(_, key, *)| dist.node_for(key) }.each do |node, cmds|
  node.pipelined do |p|
    cmds.each { |cmd, *args| p.send(cmd, *args) }
  end
end
Defensive patterns

Strategy: fallback

Validate before calling

# Branch on client class before wrapping a batch in pipelined
def batch(client, commands)
  if client.is_a?(Redis::Distributed)
    commands.group_by { |(_, key, *)| client.node_for(key) }.each do |node, cmds|
      node.pipelined { |p| cmds.each { |c, *a| p.send(c, *a) } }
    end
  else
    client.pipelined { |p| commands.each { |c, *a| p.send(c, *a) } }
  end
end

Type guard

def distributed_client?(client)
  client.is_a?(Redis::Distributed)
end

Try / catch

begin
  dist.pipelined { |p| yield p }
rescue Redis::Distributed::CannotDistribute
  # fall back to singly-issued commands; still correct, just not batched
  yield dist
end

Prevention

When it happens

Trigger: Calling dist.pipelined { |p| ... } on any Redis::Distributed instance. Commonly hit when bulk-write or read-batching code (written against a standalone client) is re-pointed at a multi-node ring, or when a caching/gem abstraction wraps command batches in pipelined internally.

Common situations: Porting bulk import/export or cache-warming code from standalone Redis to sharding; performance tuning that wraps loops of writes in pipelined; shared libraries that detect and use pipelining when available; CI suites exercising the same code against every client class.

Related errors


AI-assisted analysis of redis/redis-rb@2ba9010b91 (2026-08-23). Data as JSON: /api/errors/0364d6ace1564c34. Report an issue: GitHub.