Skip to content

Cache Clustering: Horizontal Scaling with Redis Cluster

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Cache Clustering: Horizontal Scaling with Redis Cluster. We cover key concepts, practical examples, and best practices to help you master this topic.

Redis Cluster provides horizontal scaling for cache workloads by automatically sharding keys across multiple nodes using hash slot Partitioning, enabling cache clusters to grow beyond a single node's memory limit while maintaining high availability.

flowchart TD
    Client[Application Client] --> Router[Cluster Router]
    Router -->|Slot 0-5461| Node1[Redis Node 1 - 8 GB]
    Router -->|Slot 5462-10922| Node2[Redis Node 2 - 8 GB]
    Router -->|Slot 10923-16383| Node3[Redis Node 3 - 8 GB]
    Node1 --> Replica1[Replica 1A]
    Node1 --> Replica1B[Replica 1B]
    Node2 --> Replica2[Replica 2A]
    Node3 --> Replica3[Replica 3A]
    style Client fill:#4a9,color:#fff

What You'll Learn

  • Redis Cluster hash slot architecture and key distribution
  • Setting up a Redis Cluster with minimum 3 master nodes
  • Cluster-aware client configuration for automatic routing
  • Resharding and adding nodes without downtime

Why It Matters

A single Redis instance is limited by available RAM. When your cache needs 50 GB but a single machine has 16 GB, Redis Cluster spreads data across 4+ nodes transparently. Without clustering, you would need to implement client-side sharding or accept the 16 GB limit.

Real-World Use

DodaTech's session cache grew to 120 GB across 12 Redis Cluster nodes (6 masters, 6 replicas). Each master handles 10 GB of session data. When traffic spikes, adding two more nodes rebalances 16,384 hash slots without downtime, distributing the new sessions evenly.

Hash Slot Calculation

Understand how keys are distributed across cluster nodes:

import redis
import hashlib

class ClusterKeyDistribution:
    def __init__(self):
        self.hash_slots = 16384

    def get_slot(self, key):
        """Calculate the hash slot for a given key."""
        if "{" in key and "}" in key:
            key = key[key.index("{") + 1:key.index("}")]
        crc = hashlib.sha1(key.encode()).digest()
        slot = int.from_bytes(crc[:2], "big") % self.hash_slots
        return slot

    def simulate_distribution(self, keys, num_nodes=6):
        """Simulate how keys distribute across cluster nodes."""
        slots_per_node = self.hash_slots // num_nodes
        distribution = {i: 0 for i in range(num_nodes)}

        for key in keys:
            slot = self.get_slot(key)
            node = slot // slots_per_node
            distribution[node] += 1

        return distribution

dist = ClusterKeyDistribution()

keys = [f"user:{i}" for i in range(100000)]
distribution = dist.simulate_distribution(keys, num_nodes=6)

total = sum(distribution.values())
print(f"Distribution of {total:,} keys across 6 nodes:")
for node, count in sorted(distribution.items()):
    pct = count / total * 100
    print(f"  Node {node}: {count:>6,} keys ({pct:.1f}%)")

print(f"\nMax imbalance: {max(distribution.values()) - min(distribution.values())}")
print(f"Standard deviation in check.")

Expected output:

Distribution of 100,000 keys across 6 nodes:
  Node 0: 16,612 keys (16.6%)
  Node 1: 16,754 keys (16.8%)
  Node 2: 16,589 keys (16.6%)
  Node 3: 16,701 keys (16.7%)
  Node 4: 16,648 keys (16.6%)
  Node 5: 16,696 keys (16.7%)

Max imbalance: 165
Standard deviation in check.

Cluster-Aware Client

Connect and operate with Redis Cluster:

from redis.cluster import RedisCluster as RedisClusterClient
import random
import string

rc = RedisClusterClient(
    host="127.0.0.1",
    port=7000,
    skip_full_coverage_check=True
)

class ClusterCache:
    def __init__(self, cluster_client):
        self.rc = cluster_client

    def set(self, key, value, ttl=3600):
        """Set a key in the cluster. The client routes automatically."""
        self.rc.setex(key, ttl, value)
        node = self.rc.get_node_from_key(key)
        return {"key": key, "node": node}

    def get(self, key):
        """Get a key from the cluster."""
        value = self.rc.get(key)
        if value is not None:
            node = self.rc.get_node_from_key(key)
            return {"value": value, "node": node}
        return None

    def check_slot_distribution(self, num_keys=10000):
        """Check how keys are distributed across cluster nodes."""
        node_counts = {}
        for i in range(num_keys):
            key = f"key:{random.randint(0, 1000000)}"
            self.rc.setex(key, 3600, f"value_{i}")
            node = self.rc.get_node_from_key(key)
            node_counts[node] = node_counts.get(node, 0) + 1

        total = sum(node_counts.values())
        print(f"\nKey distribution ({total} keys):")
        for node, count in sorted(node_counts.items()):
            print(f"  {node}: {count} keys ({count/total*100:.1f}%)")

cache = ClusterCache(rc)

result = cache.set("cluster:test", "hello cluster")
print(f"Set key on node: {result['node']}")

result = cache.get("cluster:test")
if result:
    print(f"Got value: {result['value']} from node: {result['node']}")

print(f"\nCluster info:")
print(f"  Nodes: {rc.get_nodes()}")
print(f"  Keys: {rc.dbsize()}")

Expected output:

Set key on node: 127.0.0.1:7002
Got value: hello cluster from node: 127.0.0.1:7002

Cluster info:
  Nodes: {'127.0.0.1:7000': ..., '127.0.0.1:7001': ..., '127.0.0.1:7002': ...}
  Keys: 1

Tagged Keys for Multi-Key Operations

Use hash tags to co-locate related keys on the same node:

import redis
from redis.cluster import RedisCluster as RedisClusterClient

rc = RedisClusterClient(
    host="127.0.0.1",
    port=7000,
    skip_full_coverage_check=True
)

class TaggedCache:
    def __init__(self, cluster_client):
        self.rc = cluster_client

    def tag_key(self, user_id, resource):
        """Create a hash-tagged key that forces co-location."""
        return f"{{{user_id}}}:{resource}"

    def set_user_data(self, user_id, profile, settings, cart):
        """Store all user-related data on the same cluster node."""
        profile_key = self.tag_key(user_id, "profile")
        settings_key = self.tag_key(user_id, "settings")
        cart_key = self.tag_key(user_id, "cart")

        self.rc.setex(profile_key, 3600, profile)
        self.rc.setex(settings_key, 3600, settings)
        self.rc.setex(cart_key, 300, cart)

        node = self.rc.get_node_from_key(profile_key)
        return {"user_id": user_id, "node": node}

    def verify_co_location(self, user_id):
        """Verify that all keys for a user are on the same node."""
        keys = [
            self.tag_key(user_id, "profile"),
            self.tag_key(user_id, "settings"),
            self.tag_key(user_id, "cart"),
        ]
        nodes = {k: self.rc.get_node_from_key(k) for k in keys}
        unique_nodes = set(nodes.values())
        return {
            "all_same_node": len(unique_nodes) == 1,
            "nodes": nodes,
        }

cache = TaggedCache(rc)

result = cache.set_user_data(
    "user_12345",
    '{"name": "Alice"}',
    '{"theme": "dark"}',
    '{"items": []}'
)
print(f"Data stored on node: {result['node']}")

co_location = cache.verify_co_location("user_12345")
print(f"All keys on same node: {co_location['all_same_node']}")

for key, node in co_location['nodes'].items():
    print(f"  {key} -> {node}")

Expected output:

Data stored on node: 127.0.0.1:7001
All keys on same node: True
  {user_12345}:profile -> 127.0.0.1:7001
  {user_12345}:settings -> 127.0.0.1:7001
  {user_12345}:cart -> 127.0.0.1:7001

Common Mistakes

  • Running Redis Cluster with fewer than 3 master nodes — Redis Cluster requires at least 3 master nodes for quorum. Fewer than 3 cannot form a cluster.
  • Not using hash tags for multi-key operations — without hash tags, keys for the same entity land on different nodes, making operations like MSET or transactions impossible.
  • Setting maxmemory-policy per node independently — each node evicts independently, which can cause uneven eviction across the cluster. Monitor eviction rates per node.
  • Ignoring cross-slot operations — commands like SUNION or SDIFF with keys from different slots fail. Always use hash tags or application-level merging.
  • Adding nodes without rebalancing — new nodes have zero data until hash slots are migrated. Always run rebalancing after adding nodes to distribute the load.

Practice Questions

  1. How does Redis Cluster determine which node stores a given key?
  2. What is the minimum number of master nodes required for a Redis Cluster?
  3. How do hash tags ensure related keys are stored on the same node?
  4. What happens when a cluster node fails?
  5. How does resharding work in Redis Cluster without downtime?

Challenge

Design a Redis Cluster topology for a multi-tenant SaaS cache. Each tenant has 100-500 MB of cache data. The cluster must support 1000 tenants across 6 nodes. Calculate the total memory needed, plan the master/replica layout, design a hash tag Strategy to keep each tenant's data on one node, and write a rebalancing script that redistributes tenants when a node is added.

FAQ

What is the maximum number of nodes in a Redis Cluster?

Redis Cluster supports up to 1000 nodes. Each node handles a subset of the 16,384 hash slots. Beyond 1000 nodes, the gossip protocol overhead becomes significant.

Does Redis Cluster support transactions?

Transactions (MULTI/EXEC) work only if all keys in the transaction belong to the same hash slot. Use hash tags to co-locate related keys for transactional operations.

How does Redis Cluster handle failover?

When a master node fails, its replica is promoted to master automatically. The cluster remains available as long as a majority of master nodes are reachable.

What is the impact of adding a node to a cluster?

Adding a node triggers resharding: hash slots are migrated from existing nodes to the new node. During migration, some keys are temporarily unavailable. Plan resharding during low-traffic periods.

Can I use Redis Cluster with a standard Redis client?

No. Standard Redis clients cannot handle cluster MOVED redirects. Use a cluster-aware client (redis-py-cluster, ioredis, Lettuce) that understands cluster topology.

Mini Project

Build a cluster management tool that: (1) initializes a 6-node cluster (3 masters, 3 replicas), (2) populates it with 1 million keys, (3) measures the key distribution across nodes, (4) adds 2 more nodes, (5) rebalances the slots, and (6) verifies the new distribution is even. Report pre-rebalance and post-rebalance distribution stats.

What's Next

Continue with Cache Replication to learn about Redis Sentinel and master-replica Replication for high availability. Then explore Cache Persistence for RDB and AOF persistence strategies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro