redis/go-redis · error

redis: Watch requires all keys to be in the same shard

Error message

redis: Watch requires all keys to be in the same shard

What it means

A Redis transaction (WATCH/MULTI/EXEC) can only run against a single server, but consistent hashing mapped the provided keys onto two or more different Ring shards. Ring.Watch detects the mismatch and returns this error instead of sending a broken transaction.

Source

Thrown at ring.go:1012

	for _, key := range keys {
		if key != "" {
			shard, err := c.sharding.GetByKey(key)
			if err != nil {
				return err
			}

			shards = append(shards, shard)
		}
	}

	if len(shards) == 0 {
		return fmt.Errorf("redis: Watch requires at least one shard")
	}

	if len(shards) > 1 {
		for _, shard := range shards[1:] {
			if shard.Client != shards[0].Client {
				err := fmt.Errorf("redis: Watch requires all keys to be in the same shard")
				return err
			}
		}
	}

	return shards[0].Client.Watch(ctx, fn, keys...)
}

// Close closes the ring client, releasing any open resources.
//
// It is rare to Close a Ring, as the Ring is meant to be long-lived
// and shared between many goroutines.
func (c *Ring) Close() error {
	c.heartbeatCancelFn()

	return c.sharding.Close()
}

View on GitHub (pinned to c5cad058c7)

Solutions

  1. Make related keys share a hash tag so they hash to the same shard: use "user1:{id}:a" and "user1:{id}:b" instead of plain key names
  2. Watch only keys known to be co-located; drop unneeded keys from the Watch list
  3. Split the logic into separate transactions per shard, or use Redis Cluster/standalone with a Lua script for atomicity across keys

Example fix

// before
ring.Watch(ctx, fn, "user:1:profile", "user:2:profile") // different shards
// after
ring.Watch(ctx, fn, "user:{1}:profile", "user:{1}:prefs") // same hash tag {1}
Defensive patterns

Strategy: validation

Validate before calling

func sameShard(ring *redis.Ring, keys ...string) bool {
    shardFor := ring.(interface{ ShardByKey(key string) (string, error) })
    s, err := shardFor.ShardByKey(keys[0])
    if err != nil { return false }
    for _, k := range keys[1:] {
        s2, err := shardFor.ShardByKey(k)
        if err != nil || s2 != s { return false }
    }
    return true
}

Try / catch

if err := ring.Watch(ctx, fn, keys...); err != nil {
    var crossShard bool = strings.Contains(err.Error(), "same shard")
    if crossShard {
        // fall back to per-shard transactions or a Lua script
        return executePerShard(ctx, keys, fn)
    }
    return err
}

Prevention

When it happens

Trigger: ringClient.Watch(ctx, fn, keyA, keyB) where keyA hashes to shard X and keyB to shard Y — different shard.Client pointers at ring.go:1012.

Common situations: Watching a key plus a lock key whose hash tags differ; sharded data layout that keeps related keys on different shards; changing weights or shard list so previously co-located keys split.

Related errors


AI-assisted analysis of redis/go-redis@c5cad058c7 (2026-09-01). Data as JSON: /api/errors/b9c33a40db236606. Report an issue: GitHub.