{"id":"b715375ccd9cc625","repo":"go-redis/redis","slug":"redis-watch-requires-all-keys-to-be-in-the-same-s","errorCode":null,"errorMessage":"redis: Watch requires all keys to be in the same slot","messagePattern":"redis: Watch requires all keys to be in the same slot","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"osscluster.go","lineNumber":38,"sourceCode":"\n\t\"github.com/redis/go-redis/v9/auth\"\n\t\"github.com/redis/go-redis/v9/internal\"\n\t\"github.com/redis/go-redis/v9/internal/hashtag\"\n\t\"github.com/redis/go-redis/v9/internal/otel\"\n\t\"github.com/redis/go-redis/v9/internal/pool\"\n\t\"github.com/redis/go-redis/v9/internal/proto\"\n\t\"github.com/redis/go-redis/v9/internal/routing\"\n\t\"github.com/redis/go-redis/v9/maintnotifications\"\n\t\"github.com/redis/go-redis/v9/push\"\n)\n\nconst (\n\tminLatencyMeasurementInterval = 10 * time.Second\n)\n\nvar (\n\terrClusterNoNodes = errors.New(\"redis: cluster has no nodes\")\n\terrNoWatchKeys    = errors.New(\"redis: Watch requires at least one key\")\n\terrWatchCrosslot  = errors.New(\"redis: Watch requires all keys to be in the same slot\")\n)\n\n// ClusterOptions are used to configure a cluster client and should be\n// passed to NewClusterClient.\ntype ClusterOptions struct {\n\t// A seed list of host:port addresses of cluster nodes.\n\tAddrs []string\n\n\t// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.\n\tClientName string\n\n\t// NewClient creates a cluster node client with provided name and options.\n\t// If NewClient is set by the user, the user is responsible for handling maintnotifications upgrades and push notifications.\n\tNewClient func(opt *Options) *Client\n\n\t// The maximum number of retries before giving up. Command is retried\n\t// on network errors and MOVED/ASK redirects.","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/osscluster.go#L20-L56","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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.","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."],"exampleFix":"// before\nerr := clusterClient.Watch(ctx, func(tx *redis.Tx) error {\n    return tx.Set(ctx, \"user:2\", newVal, 0).Err()\n}, \"user:1\", \"user:2\")\n// returns: redis: Watch requires all keys to be in the same slot\n\n// after — hash tag forces both keys into one slot\nerr := clusterClient.Watch(ctx, func(tx *redis.Tx) error {\n    return tx.Set(ctx, \"{user}:2\", newVal, 0).Err()\n}, \"{user}:1\", \"{user}:2\")","handlingStrategy":"validation","validationCode":"import \"github.com/redis/go-redis/v9/internal/hashtag\"\n\nfunc safeClusterWatch(ctx context.Context, c *redis.ClusterClient, fn func(*redis.Tx) error, keys ...string) error {\n    if len(keys) == 0 {\n        return errors.New(\"watch requires at least one key\")\n    }\n    slot := hashtag.Slot(keys[0])\n    for _, k := range keys[1:] {\n        if hashtag.Slot(k) != slot {\n            return fmt.Errorf(\"keys %q and %q are in different slots; use a hash tag like {tag}:key\", keys[0], k)\n        }\n    }\n    return c.Watch(ctx, fn, keys...)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always use Redis hash tags ({tag}) for keys involved in the same multi-key transaction or WATCH.","Validate slot equality client-side with hashtag.Slot before calling ClusterClient.Watch.","Centralize key construction in helpers that enforce a shared hash tag for related keys."],"tags":["cluster","transactions","watch","hash-slot","configuration"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}