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

  1. 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
  2. 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
  3. Restructure to a single sentinel key (e.g. one hash or one lock key) so the atomic check fits one key
  4. 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

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


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