A SaaS app is approaching 60 GB of cached session and computed-response data on a cache.r6g.xlarge (26 GB usable). The natural next move is “go bigger” — cache.r6g.4xlarge clears 105 GB. But the team also wants to spread read traffic and survive a single-AZ blip. So the architect proposes cluster mode. The PHP and Node services both use Redis. The Node service uses ioredis. The PHP service uses phpredis. One of them will work fine in cluster mode after a config flag. The other will need a refactor. Picking the wrong topology turns a capacity decision into a six-week migration.
What’s happening
ElastiCache for Redis offers two distinct topologies, and AWS names them confusingly.
Replication group (cluster mode disabled). One primary node handles all writes. Up to five read replicas serve reads. Total dataset is bounded by the primary’s memory. The cluster has a single Redis keyspace — every key lives on one node and any client can issue any command without thinking about routing. Multi-key commands (MGET, MSET, EVAL across keys) work normally. Pub/sub works normally. Transactions (MULTI/EXEC) work normally.
Cluster mode enabled. Up to 500 shards, each shard is a primary plus optional replicas. The keyspace is split across shards by CRC16 hash of the key — a key prefixed {tenant42}: always lands on the shard responsible for the hash of tenant42. The total dataset can reach hundreds of GB or TB. The catch: any multi-key operation must use keys that hash to the same slot, or it fails with CROSSSLOT. Some clients understand cluster topology natively; others need explicit upgrades or wrappers.
The decision matrix:
| Driver | Replication group | Cluster mode |
|---|---|---|
| Working set fits in one node’s memory | Right choice | Overkill |
| Working set exceeds ~100 GB | Will hit a wall | Right choice |
Heavy MGET/MSET across unrelated keys | Works as-is | Requires hash-tag rewrite |
MULTI/EXEC across keys | Works as-is | Requires hash tags |
| Lua scripts across keys | Works as-is | Requires hash tags |
| Pub/sub across all subscribers | Works as-is | Subscribers may not all see a publish |
| Read-replica reads for scale | Up to 5 replicas | Per-shard replicas |
Client library: ioredis | Works | Works with new Redis.Cluster([...]) |
Client library: lettuce (Java) | Works | Works with RedisClusterClient |
Client library: Jedis (Java) | Works | Requires JedisCluster (different API surface) |
Client library: phpredis | Works | Works with RedisCluster class (different constructor, no MULTI across keys) |
Client library: redis-py | Works | Requires redis-py-cluster historically, native in recent versions |
| Failure of one node | Replicas promote; AZ outage survivable | Per-shard failover; cross-shard outage scoped to affected slots |
The two topologies are not interchangeable at the application layer. Migrating from replication group to cluster mode means at minimum changing the client construction, and often rewriting any code that does cross-key operations.
How to verify
Look at three things before picking.
Memory ceiling. Check the working set size, not the dataset size. The working set is what stays warm — sessions touched in the last 30 days, computed responses with a 24-hour TTL. A cache.r6g.xlarge has 26.32 GB usable. A cache.r6g.4xlarge has 105.81 GB. A cache.r6g.16xlarge has 419.09 GB. If the working set is below 100 GB and not growing fast, a single-shard replication group is simpler and almost always correct.
aws elasticache describe-cache-clusters \
--show-cache-node-info \
--query 'CacheClusters[].[CacheClusterId,CacheNodeType,Engine]' \
--output table
Then check actual memory use via CloudWatch:
aws cloudwatch get-metric-statistics \
--namespace AWS/ElastiCache \
--metric-name BytesUsedForCache \
--dimensions Name=CacheClusterId,Value=saas-prod-001 \
--start-time $(date -u -v-7d +%Y-%m-%dT%H:%M:%S) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
--period 3600 \
--statistics Maximum \
--query 'Datapoints[].Maximum' \
--output text | sort -n | tail -1
If the maximum is approaching maxmemory (typically 75–80% of node memory before eviction kicks in), the node is full.
Client library inventory. List every service that talks to Redis. For each, identify the library and version. Cluster mode requires the cluster-aware constructor in most libraries. Some libraries silently degrade to non-cluster behavior and miss redirections — a bug that shows up at 2% of requests during shard rebalancing, not under normal load.
Command usage audit. Grep the codebase for MGET, MSET, MULTI, EXEC, EVAL, SUNIONSTORE, ZUNIONSTORE, RENAME, BRPOPLPUSH. If any of these operate across keys that do not share a tenant prefix, those calls will break in cluster mode unless the keys are rewritten with hash tags.
grep -rn -E "MGET|MSET|MULTI|EXEC|EVAL|RENAME" src/ \
| grep -v test \
| head -30
A common surprise: connect-redis (Node session store) issues a MULTI/EXEC block for session writes. In cluster mode this works because all keys in the block share the session-id slot, but only if the key construction uses a hash tag. If the prefix is sess:{<sid>}:, fine. If it is sess:<sid>:, the next refactor causes a cross-slot failure.
The fix
The defensible rule for picking the topology:
Default to a replication group. A cache.r6g.xlarge with one or two read replicas across AZs covers the vast majority of SaaS workloads. It is simpler operationally, simpler in code, and tolerates an AZ failure with a 30-second blip. Memory ceiling is ~26 GB, which is enough for a working set of millions of sessions plus a decent response cache.
Upgrade vertically before you go horizontal. r6g.xlarge to r6g.2xlarge to r6g.4xlarge is a single configuration change and a brief failover. You can stay on a replication group through r6g.16xlarge (419 GB) — which is enough for almost any SaaS until you have very specific reasons to shard.
Move to cluster mode when one of these is true.
- Working set exceeds ~100 GB and is still growing.
- Write throughput on the single primary is the bottleneck (above ~50k writes/sec sustained, depending on payload size).
- You need to split tenants across regions or accounts and want shard isolation per tenant tier.
- You are running Redis purely as a sharded cache where every operation is single-key (e.g. session lookups by ID) and the codebase already uses hash tags or single-key access patterns.
If you do move to cluster mode, two patterns that pay off.
Hash-tag every multi-key operation. Rewrite session keys as sess:{<sid>}:data, sess:{<sid>}:meta. The braces force both keys to hash on <sid> and land on the same shard. Apply the same pattern to per-tenant caches: tenant:{<tid>}:user:123 instead of tenant:<tid>:user:123. The {} is part of the key syntax and Redis only hashes what is inside the braces.
Test client behavior under shard rebalancing. Trigger a planned failover on one shard and run load against the app. The client should follow MOVED and ASK redirects without breaking. Libraries that do not implement redirect handling will surface as a spike of “key not found” errors during the rebalance. Catch it in staging, not production.
The honest summary: if you are not sure whether you need cluster mode, you do not. A vertically scaled replication group running on cache.r6g.4xlarge or cache.r6g.8xlarge runs most SaaS caching workloads simpler, cheaper, and with fewer footguns than a sharded cluster. Cluster mode pays for itself when the working set is the bottleneck, not when “more replicas” sounds like a good idea.
Stack Harbor designs Redis topologies as part of cluster-tier work in Managed AWS and the HA Clusters operating layer. If you are weighing a shard split against another vertical upgrade, start with an infrastructure consult.