{"record":{"id":"0364d6ace1564c34","repo":"redis/redis-rb","slug":"pipelined-cannot-be-used-in-redis-distributed-bec","errorCode":null,"errorMessage":"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.","messagePattern":"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\\.","errorType":"exception","errorClass":"Redis::Distributed::CannotDistribute","httpStatus":null,"severity":"error","filePath":"lib/redis/distributed.rb","lineNumber":1325,"sourceCode":"          node.watch(*keys, &block)\n        rescue StandardError\n          @watch_key = nil\n          raise\n        end\n      end\n    end\n\n    # Forget about all watched keys.\n    def unwatch\n      raise CannotDistribute, :unwatch unless @watch_key\n\n      result = node_for(@watch_key).unwatch\n      @watch_key = nil\n      result\n    end\n\n    def pipelined\n      raise CannotDistribute, :pipelined\n    end\n\n    # Mark the start of a transaction block.\n    def multi(&block)\n      raise CannotDistribute, :multi unless @watch_key\n\n      node_for(@watch_key).multi(&block)\n    end\n\n    # Execute all commands issued after MULTI.\n    def exec\n      raise CannotDistribute, :exec unless @watch_key\n\n      result = node_for(@watch_key).exec\n      @watch_key = nil\n      result\n    end\n","sourceCodeStart":1307,"sourceCodeEnd":1343,"githubUrl":"https://github.com/redis/redis-rb/blob/2ba9010b91dab9e0fde1fbae3a9aae003f8bc307/lib/redis/distributed.rb#L1307-L1343","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\ndist.pipelined do |p|\n  p.set(\"k1\", \"a\")\n  p.incr(\"k2\")\nend\n# => Redis::Distributed::CannotDistribute\n\n# after: group commands by node, one pipeline per node\ncommands = [[:set, \"k1\", \"a\"], [:incr, \"k2\"]]\ncommands.group_by { |(_, key, *)| dist.node_for(key) }.each do |node, cmds|\n  node.pipelined do |p|\n    cmds.each { |cmd, *args| p.send(cmd, *args) }\n  end\nend","handlingStrategy":"fallback","validationCode":"# Branch on client class before wrapping a batch in pipelined\ndef batch(client, commands)\n  if client.is_a?(Redis::Distributed)\n    commands.group_by { |(_, key, *)| client.node_for(key) }.each do |node, cmds|\n      node.pipelined { |p| cmds.each { |c, *a| p.send(c, *a) } }\n    end\n  else\n    client.pipelined { |p| commands.each { |c, *a| p.send(c, *a) } }\n  end\nend","typeGuard":"def distributed_client?(client)\n  client.is_a?(Redis::Distributed)\nend","tryCatchPattern":"begin\n  dist.pipelined { |p| yield p }\nrescue Redis::Distributed::CannotDistribute\n  # fall back to singly-issued commands; still correct, just not batched\n  yield dist\nend","preventionTips":["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"],"tags":["redis","ruby","distributed","client-side-sharding","pipelining","performance"],"backgroundTag":"redis-distributed-cannot-distribute","analyzedSha":"2ba9010b91dab9e0fde1fbae3a9aae003f8bc307","analyzedAt":"2026-08-23T03:54:57.017Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}