redis/redis-rb · error · Redis::Distributed::CannotDistribute
MAPPED_MSETNX cannot be used in Redis::Distributed because t
Error message
MAPPED_MSETNX 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 shards keys across N independent standalone Redis servers using a client-side consistent-hash ring (CRC32 over key or {tag}). MSETNX must set every key only if none of them exists, which the server can only guarantee when all keys live on one node. Because the ring may route the hash's keys to different nodes, the client raises CannotDistribute instead of silently breaking the all-or-nothing guarantee. Note that both msetnx and mapped_msetnx raise unconditionally — unlike tag-aware commands (e.g. geosearchstore) that use ensure_same_node, no key-tag arrangement makes them callable on the facade.
Source
Thrown at lib/redis/distributed.rb:340
node_for(key).setnx(key, value)
end
# Set multiple keys to multiple values.
def mset(*)
raise CannotDistribute, :mset
end
def mapped_mset(_hash)
raise CannotDistribute, :mapped_mset
end
# Set multiple keys to multiple values, only if none of the keys exist.
def msetnx(*)
raise CannotDistribute, :msetnx
end
def mapped_msetnx(_hash)
raise CannotDistribute, :mapped_msetnx
end
# Get the value of a key.
def get(key)
node_for(key).get(key)
end
# Get the value of a key and delete it.
def getdel(key)
node_for(key).getdel(key)
end
# Get the value of a key and sets its time to live based on options.
def getex(key, **options)
node_for(key).getex(key, **options)
end
# Set the JSON value at a path in the document stored under a key.View on GitHub (pinned to 2ba9010b91)
Solutions
- If cross-key atomicity is not required, replace with per-key conditional writes: hash.each { |k, v| dist.set(k, v, nx: true) } and check which keys returned true
- If the keys can share a hash tag, route to the single node yourself: dist.node_for("{tag}k1").mapped_msetnx(hash) — node_for honors the {tag} regex, and the underlying standalone client supports the command
- Restructure to a single sentinel key (e.g. one hash or one lock key) so the atomic check fits one key
- If the workload genuinely needs multi-key atomicity at scale, move to Redis::Cluster, where hash tags place keys in one server-side slot and MSETNX works
Example fix
# before
dist.mapped_msetnx({ "k1" => "a", "k2" => "b" })
# => Redis::Distributed::CannotDistribute
# after (option 1: per-key, not atomic across keys)
written = { "k1" => "a", "k2" => "b" }.each_with_object({}) do |(k, v), acc|
acc[k] = dist.set(k, v, nx: true)
end
# after (option 2: shared hash tag, atomic on one node)
dist.node_for("{job1}k1").mapped_msetnx({ "{job1}k1" => "a", "{job1}k2" => "b" }) Defensive patterns
Strategy: fallback
Validate before calling
# Route multi-key conditional writes through an adapter before touching the client
def safe_mapped_msetnx(client, hash)
if client.is_a?(Redis::Distributed)
hash.each_with_object({}) { |(k, v), acc| acc[k] = client.set(k, v, nx: true) }
else
client.mapped_msetnx(hash)
end
end Type guard
def distributed_client?(client) client.is_a?(Redis::Distributed) end
Try / catch
begin
dist.mapped_msetnx(hash)
rescue Redis::Distributed::CannotDistribute => e
logger.warn("#{e.message}; falling back to per-key set nx")
hash.each { |k, v| dist.set(k, v, nx: true) }
end Prevention
- Treat Redis::Distributed as a strictly smaller API than Redis: audit for multi-key atomic commands (msetnx, mapped_msetnx, json_mset, and friends) before switching client classes
- Keep multi-key atomic operations behind one adapter method so the fallback lives in exactly one place
- Run your test suite against Redis::Distributed (rake test:distributed) before adopting it in production paths
- Document which operations trade atomicity for shardability next to the adapter
When it happens
Trigger: Calling dist.mapped_msetnx({"k1" => "a", "k2" => "b"}) or dist.msetnx("k1", "a", "k2", "b") on any Redis::Distributed instance, regardless of key tags. Typically code that ran against a standalone Redis client and is re-pointed at a Distributed client (Redis.new([...urls]) or Redis::Distributed.new), or shared library code that accepts either client class.
Common situations: Porting an app from one Redis server to a shard fleet; caching/session libraries (e.g. lock or claim-a-set-of-keys patterns such as idempotency markers) that call mapped_msetnx internally; test suites that exercise the same command layer against both Redis and Redis::Distributed clients; Sidekiq/Resque-style atomic multi-key claims.
Related errors
- JSON_MSET cannot be used in Redis::Distributed because the k
- PIPELINED 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/8acd84f2faa3cdf9.
Report an issue: GitHub.