Skip to main content

Command Palette

Search for a command to run...

Distributed Caching: Redis Cluster

Published
11 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Why Traditional Caching Approaches Fail at Scale

Single-instance Redis deployments hit hard limits around 25GB of memory and 100,000 operations per second on modern hardware. Vertical scaling becomes prohibitively expensive, and replica-only architectures create write bottlenecks. Master-replica setups with Sentinel provide high availability but don't solve the fundamental problem: all data still resides on a single master node.

The shift toward microservices, serverless functions, and edge computing in 2025 demands cache infrastructure that distributes both data and load across multiple nodes. Applications now generate cache keys with unpredictable cardinality—user sessions, ML model predictions, real-time analytics—making it impossible to manually partition data. Privacy regulations like GDPR and data residency requirements force teams to maintain cache clusters in specific geographic regions while ensuring consistent performance globally.

Managed Redis services abstract away complexity but introduce vendor lock-in, limited configuration control, and cost structures that become unsustainable at scale. Teams processing terabytes of cached data need fine-grained control over sharding strategies, memory policies, and network topology that managed services don't provide.

Understanding Redis Cluster Architecture

Redis Cluster configuration implements automatic sharding across 16,384 hash slots distributed among master nodes. Each key maps to a specific slot using CRC16 hashing, and the cluster automatically routes requests to the correct node. This architecture enables horizontal scaling while maintaining the performance characteristics Redis is known for.

Unlike consistent hashing used in other distributed systems, Redis Cluster's hash slot approach allows for deterministic key distribution and efficient cluster resharding. When you add or remove nodes, only specific hash slots migrate—not entire key ranges. This design minimizes data movement during topology changes.

A production Redis Cluster requires at minimum three master nodes to form a quorum and handle split-brain scenarios correctly. Each master should have at least one replica for high availability. The cluster uses a gossip protocol for node discovery and failure detection, with each node maintaining a view of the entire cluster topology.

Production-Grade Redis Cluster Configuration

Here's a realistic Redis Cluster setup for a high-traffic application handling 500,000 requests per second with 200GB of cached data:

# redis-cluster-node-1.conf
port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
cluster-replica-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
appendonly yes
appendfsync everysec
maxmemory 32gb
maxmemory-policy allkeys-lru
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
supervised no
loglevel notice
databases 1
save ""
stop-writes-on-bgsave-error no
rdbcompression yes
rdbchecksum yes
dir /var/lib/redis/cluster
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync yes
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes

Critical configuration decisions here address real production challenges. Setting cluster-require-full-coverage no prevents the entire cluster from becoming unavailable when a subset of hash slots goes offline—essential for maintaining partial availability during node failures. The cluster-replica-validity-factor of 10 ensures replicas don't promote themselves to master too aggressively during temporary network issues.

Memory management becomes crucial at scale. The allkeys-lru eviction policy works best for general caching workloads where any key can be evicted. For session stores or rate limiters, use volatile-lru to only evict keys with TTL set. The lazyfree options enable background deletion of large keys, preventing blocking operations that cause latency spikes.

Implementing Cluster-Aware Client Logic

Modern Redis clients must understand cluster topology and handle redirections efficiently. Here's a production TypeScript implementation using ioredis:

import Redis from 'ioredis';

interface ClusterConfig {
  nodes: Array<{ host: string; port: number }>;
  options: {
    enableReadyCheck: boolean;
    maxRetriesPerRequest: number;
    retryDelayOnFailover: number;
    retryDelayOnClusterDown: number;
    scaleReads: string;
    slotsRefreshTimeout: number;
    clusterRetryStrategy: (times: number) => number | null;
  };
}

class DistributedCacheClient {
  private cluster: Redis.Cluster;
  private readonly maxRetries = 3;

  constructor(config: ClusterConfig) {
    this.cluster = new Redis.Cluster(config.nodes, {
      ...config.options,
      redisOptions: {
        enableOfflineQueue: false,
        connectTimeout: 10000,
        commandTimeout: 5000,
        keepAlive: 30000,
        family: 4,
      },
      clusterRetryStrategy: (times: number) => {
        if (times > this.maxRetries) {
          return null; // Stop retrying
        }
        return Math.min(times * 100, 2000);
      },
    });

    this.setupEventHandlers();
  }

  private setupEventHandlers(): void {
    this.cluster.on('error', (err) => {
      console.error('Cluster error:', err);
      // Implement circuit breaker logic here
    });

    this.cluster.on('node error', (err, address) => {
      console.error(`Node ${address} error:`, err);
      // Track node-specific failures for monitoring
    });

    this.cluster.on('+node', (node) => {
      console.log('New node added:', node.options.host);
    });

    this.cluster.on('-node', (node) => {
      console.log('Node removed:', node.options.host);
    });
  }

  async getWithFallback<T>(
    key: string,
    fallbackFn: () => Promise<T>,
    ttl: number = 3600
  ): Promise<T> {
    try {
      const cached = await this.cluster.get(key);
      if (cached) {
        return JSON.parse(cached) as T;
      }
    } catch (err) {
      console.error(`Cache read failed for key ${key}:`, err);
      // Continue to fallback
    }

    const value = await fallbackFn();

    // Fire-and-forget cache write to avoid blocking
    this.cluster.setex(key, ttl, JSON.stringify(value)).catch((err) => {
      console.error(`Cache write failed for key ${key}:`, err);
    });

    return value;
  }

  async mgetWithPipeline(keys: string[]): Promise<Map<string, any>> {
    const pipeline = this.cluster.pipeline();
    keys.forEach((key) => pipeline.get(key));

    const results = await pipeline.exec();
    const resultMap = new Map<string, any>();

    results?.forEach(([err, value], index) => {
      if (!err && value) {
        try {
          resultMap.set(keys[index], JSON.parse(value as string));
        } catch (parseErr) {
          console.error(`Parse error for key ${keys[index]}:`, parseErr);
        }
      }
    });

    return resultMap;
  }

  async invalidatePattern(pattern: string): Promise<number> {
    let cursor = '0';
    let deletedCount = 0;
    const batchSize = 1000;

    do {
      const [newCursor, keys] = await this.cluster.scan(
        cursor,
        'MATCH',
        pattern,
        'COUNT',
        batchSize
      );
      cursor = newCursor;

      if (keys.length > 0) {
        const pipeline = this.cluster.pipeline();
        keys.forEach((key) => pipeline.del(key));
        await pipeline.exec();
        deletedCount += keys.length;
      }
    } while (cursor !== '0');

    return deletedCount;
  }

  async disconnect(): Promise<void> {
    await this.cluster.quit();
  }
}

// Production usage
const cacheClient = new DistributedCacheClient({
  nodes: [
    { host: '10.0.1.10', port: 7000 },
    { host: '10.0.1.11', port: 7001 },
    { host: '10.0.1.12', port: 7002 },
  ],
  options: {
    enableReadyCheck: true,
    maxRetriesPerRequest: 3,
    retryDelayOnFailover: 100,
    retryDelayOnClusterDown: 300,
    scaleReads: 'slave',
    slotsRefreshTimeout: 2000,
    clusterRetryStrategy: (times) => Math.min(times * 50, 2000),
  },
});

export default cacheClient;

This implementation handles critical production scenarios: connection pooling, automatic retries with exponential backoff, graceful degradation when cache is unavailable, and efficient batch operations using pipelines. The scaleReads: 'slave' configuration distributes read traffic across replicas, reducing load on master nodes.

Cluster Topology and Sharding Strategy

Optimal Redis Cluster configuration depends on your data access patterns and infrastructure constraints. For a 6-node cluster (3 masters, 3 replicas) handling 200GB of data:

Master Node Distribution:

  • Node 1: Slots 0-5460 (~33% of hash space)
  • Node 2: Slots 5461-10922 (~33% of hash space)
  • Node 3: Slots 10923-16383 (~34% of hash space)

Each master should run on separate physical hosts or availability zones. Replicas must be distributed to ensure no master and its replica share the same failure domain. In cloud environments, spread nodes across availability zones but within the same region to minimize replication latency.

For applications with hot keys (frequently accessed data), use hash tags to control key distribution:

// Keys with same hash tag land on same node
const userSessionKey = `session:{user:12345}:data`;
const userPrefsKey = `prefs:{user:12345}:settings`;
// Both keys route to same slot due to {user:12345} hash tag

This enables atomic multi-key operations and reduces cross-node communication. However, overusing hash tags can create unbalanced clusters where specific nodes handle disproportionate traffic.

Handling Cluster Resharding and Scaling

Adding capacity to a running Redis Cluster requires careful orchestration. The resharding process migrates hash slots from existing nodes to new nodes while maintaining availability:

# Add new nodes to cluster
redis-cli --cluster add-node 10.0.1.13:7003 10.0.1.10:7000
redis-cli --cluster add-node 10.0.1.14:7004 10.0.1.10:7000 --cluster-slave

# Reshard slots to new master
redis-cli --cluster reshard 10.0.1.10:7000 \
  --cluster-from <source-node-id> \
  --cluster-to <new-node-id> \
  --cluster-slots 2730 \
  --cluster-yes

# Verify cluster health
redis-cli --cluster check 10.0.1.10:7000

During resharding, Redis migrates keys in small batches to minimize impact. The cluster remains available, but performance may degrade slightly. Schedule resharding during low-traffic periods and monitor key migration rates.

For predictable scaling, pre-shard your cluster with more nodes than immediately necessary. Starting with 6 masters instead of 3 provides headroom for traffic growth without requiring resharding. The memory overhead of additional nodes is often cheaper than the operational complexity of frequent topology changes.

Common Pitfalls and Failure Modes

Split-Brain Scenarios: Network partitions can cause cluster splits where nodes disagree on topology. Redis Cluster uses majority quorum for master election, but with only 3 masters, losing 2 nodes makes the cluster unavailable. Always deploy at least 5 master nodes in production for better partition tolerance.

Slot Migration Failures: If a node crashes during resharding, some slots may be in an inconsistent state. The cluster marks these slots as MIGRATING or IMPORTING indefinitely. Manual intervention using CLUSTER SETSLOT commands is required to resolve these states.

Memory Fragmentation: Long-running clusters develop memory fragmentation, where allocated memory exceeds actual data size by 50% or more. Monitor fragmentation ratio and restart nodes periodically during maintenance windows. Enable activedefrag yes in Redis 7.x to reduce fragmentation automatically.

Cross-Slot Command Failures: Multi-key operations like MGET or MSET fail if keys map to different slots. Applications must either use hash tags to colocate related keys or implement client-side scatter-gather logic for cross-slot operations.

Replica Promotion Delays: When a master fails, replicas take 5-15 seconds to detect the failure and elect a new master. During this window, writes to affected slots fail. Applications need retry logic and circuit breakers to handle these transient failures gracefully.

Thundering Herd on Cache Misses: When a popular key expires, multiple requests simultaneously query the backing database. Implement probabilistic early expiration or use Redis locks to ensure only one request regenerates the cached value:

async getWithLock<T>(
  key: string,
  lockKey: string,
  fetchFn: () => Promise<T>,
  ttl: number
): Promise<T> {
  const cached = await this.cluster.get(key);
  if (cached) return JSON.parse(cached);

  const lockAcquired = await this.cluster.set(
    lockKey,
    '1',
    'EX',
    10,
    'NX'
  );

  if (lockAcquired) {
    try {
      const value = await fetchFn();
      await this.cluster.setex(key, ttl, JSON.stringify(value));
      return value;
    } finally {
      await this.cluster.del(lockKey);
    }
  }

  // Wait briefly and retry
  await new Promise((resolve) => setTimeout(resolve, 100));
  return this.getWithLock(key, lockKey, fetchFn, ttl);
}

Best Practices for Production Redis Clusters

Infrastructure Requirements:

  • Minimum 3 master nodes, 1 replica per master
  • 16GB+ RAM per node for production workloads
  • SSD storage for persistence (AOF/RDB)
  • 10Gbps network between cluster nodes
  • Separate monitoring and management network

Configuration Checklist:

  • Enable cluster mode with cluster-enabled yes
  • Set cluster-require-full-coverage no for partial availability
  • Configure maxmemory to 75% of available RAM
  • Use appendonly yes with appendfsync everysec for durability
  • Enable lazy freeing with lazyfree-lazy-* options
  • Set tcp-backlog 511 and tune kernel net.core.somaxconn
  • Disable RDB snapshots with save "" if using AOF

Monitoring Metrics:

  • Cluster state and slot coverage
  • Node memory usage and fragmentation ratio
  • Command latency (p50, p99, p999)
  • Network bandwidth between nodes
  • Keyspace hit rate and eviction count
  • Replication lag between masters and replicas

Operational Procedures:

  • Automate cluster health checks every 30 seconds
  • Set up alerts for node failures and slot migration
  • Implement automated failover testing monthly
  • Schedule rolling restarts quarterly to reduce fragmentation
  • Maintain runbooks for common failure scenarios
  • Test backup and restore procedures regularly

Security Hardening:

  • Enable TLS for client and cluster bus connections
  • Use requirepass for authentication
  • Implement network segmentation with VPCs
  • Restrict CONFIG and SCRIPT commands
  • Enable audit logging for compliance requirements

Frequently Asked Questions

What is the minimum number of nodes for a production Redis Cluster?

A production Redis Cluster requires at least 6 nodes: 3 master nodes to form a quorum and handle split-brain scenarios, plus 3 replica nodes for high availability. With fewer than 3 masters, the cluster cannot maintain majority consensus during network partitions. Each master should have at least one replica to ensure automatic failover when a master fails.

How does Redis Cluster handle network partitions in 2025?

Redis Cluster uses a quorum-based approach where a master can only accept writes if it can communicate with the majority of other masters. During a network partition, the minority partition becomes read-only to prevent split-brain scenarios. Nodes in the minority partition detect they've lost quorum and stop accepting writes until connectivity is restored. This design prioritizes consistency over availability during partitions.

What is the best way to migrate from single-instance Redis to Redis Cluster?

Use Redis's built-in replication to create a cluster replica of your single instance, then promote it to a cluster master. First, deploy your Redis Cluster with one master node. Configure your single instance as a replica of this cluster node using REPLICAOF. Once replication catches up, switch your application to cluster-aware clients, then add additional master nodes and reshard. This approach minimizes downtime and allows gradual migration.

When should you avoid using Redis Cluster?

Avoid Redis Cluster when your dataset fits comfortably on a single node (under 20GB), your application requires frequent multi-key transactions across unrelated keys, or you need Lua scripts that access keys across multiple slots. For these scenarios, a single Redis instance with Sentinel for high availability provides simpler operations and better performance. Redis Cluster's complexity only pays off when you need horizontal scaling.

How do you scale Redis Cluster reads without adding master nodes?

Configure your cluster-aware client to route read operations to replica nodes using the READONLY command and scaleReads: 'slave' option. This distributes read traffic across replicas while keeping writes on masters. For read-heavy workloads, add more replicas per master (2-3 replicas per master is common). Ensure your application can tolerate eventual consistency, as replicas may lag behind masters by milliseconds.

What causes Redis Cluster slot migration to fail?

Slot migration fails when source or destination nodes crash mid-migration, network connectivity drops during key transfer, or nodes run out of memory. Failed migrations leave slots in MIGRATING or IMPORTING states. To recover, identify affected slots with CLUSTER NODES, then manually fix slot assignments using CLUSTER SETSLOT <slot> STABLE on all nodes. Always monitor migration progress and avoid resharding during peak traffic.

How do you handle Redis Cluster configuration in Kubernetes?

Use StatefulSets for stable network identities and persistent storage. Deploy each Redis node as a separate pod with anti-affinity rules to spread nodes across Kubernetes nodes. Use headless services for cluster