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
- Drop the pipelined block and issue the commands singly through the facade — correct everywhere, one round trip per command
- 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
- If the batch targets known keys, build per-node buckets first (key -> dist.node_for(key)), then run one pipeline per bucket
- 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
- Audit for pipelined blocks before moving a codebase onto Redis::Distributed — caching and bulk-loading code is the usual offender
- Wrap batching in one helper so the per-node grouping fallback lives in a single place
- Consider Redis::Cluster when pipelining throughput matters — its client fans pipelines out across slots natively
- Benchmark the singly-issued fallback before assuming you need pipelines at all
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
- MAPPED_MSETNX cannot be used in Redis::Distributed because t
- JSON_MSET cannot be used in Redis::Distributed because the k
- Can't unsubscribe if not subscribed.
- UNWATCH cannot be used in Redis::Distributed because the key
AI-assisted analysis of redis/redis-rb@2ba9010b91 (2026-08-23).
Data as JSON: /api/errors/0364d6ace1564c34.
Report an issue: GitHub.