go-redis/redis · error

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

Error message

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

What it means

Returned by ClusterClient.Watch when the watched keys do not all hash to the same cluster slot. Redis Cluster cannot run a MULTI/EXEC transaction (WATCH) across multiple slots, so go-redis rejects this client-side before contacting any node. Each key is hashed with hashtag.Slot; if any differ from the first key's slot, the error is returned immediately.

Source

Thrown at osscluster.go:38

	"github.com/redis/go-redis/v9/auth"
	"github.com/redis/go-redis/v9/internal"
	"github.com/redis/go-redis/v9/internal/hashtag"
	"github.com/redis/go-redis/v9/internal/otel"
	"github.com/redis/go-redis/v9/internal/pool"
	"github.com/redis/go-redis/v9/internal/proto"
	"github.com/redis/go-redis/v9/internal/routing"
	"github.com/redis/go-redis/v9/maintnotifications"
	"github.com/redis/go-redis/v9/push"
)

const (
	minLatencyMeasurementInterval = 10 * time.Second
)

var (
	errClusterNoNodes = errors.New("redis: cluster has no nodes")
	errNoWatchKeys    = errors.New("redis: Watch requires at least one key")
	errWatchCrosslot  = errors.New("redis: Watch requires all keys to be in the same slot")
)

// ClusterOptions are used to configure a cluster client and should be
// passed to NewClusterClient.
type ClusterOptions struct {
	// A seed list of host:port addresses of cluster nodes.
	Addrs []string

	// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
	ClientName string

	// NewClient creates a cluster node client with provided name and options.
	// If NewClient is set by the user, the user is responsible for handling maintnotifications upgrades and push notifications.
	NewClient func(opt *Options) *Client

	// The maximum number of retries before giving up. Command is retried
	// on network errors and MOVED/ASK redirects.

View on GitHub (pinned to 36d97525cd)

Solutions

  1. Wrap all watched keys in a Redis hash tag so they share a slot, e.g. "{user}:1" and "{user}:2" — the substring inside {} determines the slot.
  2. If the keys are genuinely unrelated and must be on different slots, restructure to avoid multi-key transactions; use per-key independent operations or Lua scripts scoped to one key.
  3. Fall back to a non-cluster Client (single endpoint) or a Ring if you truly need cross-key WATCH semantics, though this sacrifices cluster scaling.

Example fix

// before
err := clusterClient.Watch(ctx, func(tx *redis.Tx) error {
    return tx.Set(ctx, "user:2", newVal, 0).Err()
}, "user:1", "user:2")
// returns: redis: Watch requires all keys to be in the same slot

// after — hash tag forces both keys into one slot
err := clusterClient.Watch(ctx, func(tx *redis.Tx) error {
    return tx.Set(ctx, "{user}:2", newVal, 0).Err()
}, "{user}:1", "{user}:2")
Defensive patterns

Strategy: validation

Validate before calling

import "github.com/redis/go-redis/v9/internal/hashtag"

func safeClusterWatch(ctx context.Context, c *redis.ClusterClient, fn func(*redis.Tx) error, keys ...string) error {
    if len(keys) == 0 {
        return errors.New("watch requires at least one key")
    }
    slot := hashtag.Slot(keys[0])
    for _, k := range keys[1:] {
        if hashtag.Slot(k) != slot {
            return fmt.Errorf("keys %q and %q are in different slots; use a hash tag like {tag}:key", keys[0], k)
        }
    }
    return c.Watch(ctx, fn, keys...)
}

Prevention

When it happens

Trigger: Calling ClusterClient.Watch(ctx, fn, key1, key2, ...) where two or more keys map to different hash slots, e.g. Watch(ctx, fn, "user:1", "user:2"). Without hash tags these land on different slots. Only ClusterClient enforces this; single-node Client.Watch has no slot concept.

Common situations: Migrating from standalone Redis to Redis Cluster without realizing WATCH needs same-slot keys. Using natural key names ("user:1", "user:2") that scatter across slots. Attempting multi-key optimistic locking patterns (CAS) that worked on standalone Redis.

Related errors


AI-assisted analysis of go-redis/redis@36d97525cd (2026-08-06). Data as JSON: /data/errors/b715375ccd9cc625.json. Report an issue: GitHub.