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

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

Error message

JSON_MSET 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 standalone servers via a client-side hash ring. JSON.MSET is a single atomic (transactional) command that sets several JSON documents at once, and the server can only keep that guarantee when every key is on one node. Since the ring may place the triplet keys on different nodes, the client raises CannotDistribute up front rather than issue a partially-applied multi-document write. The raise is unconditional — even when all keys happen to hash to the same node.

Source

Thrown at lib/redis/distributed.rb:371

    # 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.
    def json_set(key, path, value, **options)
      node_for(key).json_set(key, path, value, **options)
    end

    # Get the JSON value(s) at one or more paths in the document stored under a key.
    def json_get(key, *paths, **options)
      node_for(key).json_get(key, *paths, **options)
    end

    # Set one or more JSON values. The keys may live on different nodes and the operation must be
    # atomic, so it cannot be distributed.
    def json_mset(*)
      raise CannotDistribute, :json_mset
    end

    # Get the values at a path from several keys. Keys are grouped by node, queried per node, and
    # reassembled in the original key order.
    def json_mget(*keys, path, **options)
      keys.flatten!(1)
      values = keys.group_by { |key| node_for(key) }.each_with_object({}) do |(node, subkeys), acc|
        node.json_mget(*subkeys, path, **options).each_with_index do |value, i|
          acc[subkeys[i]] = value
        end
      end
      keys.map { |key| values[key] }
    end

    # Delete the JSON value(s) at a path in the document stored under a key.
    def json_del(key, path = nil)
      node_for(key).json_del(key, path)
    end

View on GitHub (pinned to 2ba9010b91)

Solutions

  1. Write each document individually: dist.json_set(key, "$", value) per triplet — correct across shards, but loses multi-document atomicity
  2. If the keys can share a hash tag, run the command on that one node: dist.node_for("{tag}doc1").json_mset("{tag}doc1", "$", v1, "{tag}doc2", "$", v2)
  3. Wrap the writes in your own cross-node compensation (write, verify, roll back) only if you truly need all-or-nothing behavior
  4. If atomic multi-key JSON writes are a hard requirement, switch topology to Redis::Cluster and use hash tags for co-location

Example fix

# before
dist.json_mset("doc1", "$", { "a" => 1 }, "doc2", "$", { "b" => 2 })
# => Redis::Distributed::CannotDistribute

# after (per-key writes)
dist.json_set("doc1", "$", { "a" => 1 })
dist.json_set("doc2", "$", { "b" => 2 })

# after (shared hash tag, atomic on one node)
dist.node_for("{user1}doc1").json_mset("{user1}doc1", "$", { "a" => 1 }, "{user1}doc2", "$", { "b" => 2 })
Defensive patterns

Strategy: fallback

Validate before calling

# Branch on client class before issuing a multi-document JSON write
def safe_json_mset(client, *triplets, raw: false)
  if client.is_a?(Redis::Distributed)
    triplets.each_slice(3) { |key, path, value| client.json_set(key, path, value, raw: raw) }
  else
    client.json_mset(*triplets, raw: raw)
  end
end

Type guard

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

Try / catch

begin
  dist.json_mset(*triplets)
rescue Redis::Distributed::CannotDistribute => e
  logger.warn("#{e.message}; falling back to per-key json_set")
  triplets.each_slice(3) { |key, path, value| dist.json_set(key, path, value) }
end

Prevention

When it happens

Trigger: Calling dist.json_mset("doc1", "$", v1, "doc2", "$", v2) (flat key/path/value triplets) on a Redis::Distributed client. Hit when JSON-bulk-loading or document-sync code written for a standalone Redis (RedisJSON / Redis 8.0+) runs against a Distributed topology.

Common situations: Migrating an app that uses RedisJSON documents from standalone Redis to client-side sharding; shared model/persistence layers that accept either Redis or Redis::Distributed; batch document upserts that used JSON.MSET for atomicity; upgrading a gem that newly delegates to json_mset internally.

Related errors


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