Guide · AWS ElastiCache
MCP Server ElastiCache Cluster Mode — hash slots, MOVED redirects, multi-key commands
ElastiCache cluster mode enabled (CME) distributes your keyspace across multiple shards — each shard is a primary node plus one or more replicas. Three things break applications that move from single-node to cluster mode without changes: multi-key commands fail if keys span shards — MGET key1 key2 returns CROSSSLOT Keys in request don't hash to the same slot if key1 and key2 are in different hash slots; pub/sub routing is complex in cluster mode — PUBLISH and SUBSCRIBE only work on the shard that owns the channel's hash slot, so ioredis must route subscribe commands to all shards; and Lua scripts must constrain their key access to a single slot — ElastiCache rejects EVAL scripts that access keys across multiple slots because the script might need to redirect mid-execution. Use hash tags — wrapping part of a key name in {} braces — to force related keys to the same hash slot.
TL;DR
Use ioredis.Cluster instead of new Redis(). Group related MCP session keys under the same hash tag (e.g., {session:abc123}:state and {session:abc123}:tools) so they land on the same shard and can be read together with MGET or a pipeline. Avoid multi-key commands with keys that have no hash tag — they will hit the CROSSSLOT error. Use scaleReads: 'slave' to spread read load across replicas.
How cluster mode distributes keys
Redis cluster mode uses 16,384 hash slots. Every key maps to a slot via CRC16(key) % 16384. The slots are divided evenly across shards. With 3 shards, each shard owns approximately 5,461 slots:
- Shard 0 (primary A): slots 0–5460
- Shard 1 (primary B): slots 5461–10922
- Shard 2 (primary C): slots 10923–16383
When a client sends a command for a key that belongs to a different shard, the node returns a MOVED redirect: MOVED 7638 10.0.1.5:6379. The number is the hash slot; the address is the primary node that owns it. ioredis Cluster handles MOVED transparently — it updates its slot map and re-sends the command to the correct node.
During re-sharding (moving slots from one shard to another), the source shard returns ASK redirects for keys in the migrating slots: ASK 7638 10.0.2.5:6379. The client must send an ASKING command to the destination node before the actual command. ioredis Cluster handles ASK automatically. ASK is temporary; once migration is complete, the slot map is updated and MOVED takes over.
Connecting ioredis in cluster mode
Pass one or more seed nodes to new Redis.Cluster(). ioredis uses CLUSTER SLOTS (or CLUSTER SHARDS on Redis 7+) to discover all nodes and build the routing table automatically.
import Redis from "ioredis";
const cluster = new Redis.Cluster(
[
// Seed nodes — ioredis discovers the full topology from any one of these
{ host: process.env.ELASTICACHE_CONFIG_ENDPOINT!, port: 6379 },
],
{
redisOptions: {
password: process.env.REDIS_AUTH_TOKEN,
tls: {}, // required when transit encryption is enabled
connectTimeout: 5000,
},
// Read from replicas to spread read load across AZs
scaleReads: "slave",
// How long to wait before retrying after a CLUSTERDOWN error
clusterRetryStrategy: (times: number) => Math.min(times * 500, 5000),
// Do not throw on LOADING errors during node startup
enableOfflineQueue: true,
}
);
cluster.on("error", (err) => {
console.error("ElastiCache Cluster error", err.message);
});
cluster.on("+node", (node) => {
console.log(`Discovered cluster node: ${node.options.host}`);
});
ElastiCache cluster mode exposes a configuration endpoint (distinct from the individual shard primary endpoints) specifically designed for cluster clients to use as the seed. Use the configuration endpoint in the Cluster constructor — do not hardcode individual shard addresses, as they change on scaling events and node replacements.
Multi-key commands and CROSSSLOT errors
In cluster mode, commands that operate on multiple keys — MGET, MSET, DEL key1 key2, SUNION, SINTERSTORE — only succeed if all keys hash to the same slot. If they span slots, Redis returns:
CROSSSLOT Keys in request don't hash to the same slot
Two strategies to handle this:
Strategy 1: Hash tags for co-location
Wrap the shared part of a key name in curly braces. Redis computes the hash slot from only the content between the first { and }, ignoring the rest. Keys with the same hash tag are guaranteed to land in the same slot.
// These keys all hash on "session:abc123" — same slot, same shard
const sessionId = "abc123";
// Store different aspects of one MCP session together
await cluster.mset(
`{session:${sessionId}}:state`, JSON.stringify(state),
`{session:${sessionId}}:tools`, JSON.stringify(tools),
`{session:${sessionId}}:cursor`, JSON.stringify(cursor)
);
// Read all three in one round trip — allowed because same hash tag = same slot
const [rawState, rawTools, rawCursor] = await cluster.mget(
`{session:${sessionId}}:state`,
`{session:${sessionId}}:tools`,
`{session:${sessionId}}:cursor`
);
// Set TTL on all three atomically via a pipeline (same slot = same node)
const pipeline = cluster.pipeline();
pipeline.expire(`{session:${sessionId}}:state`, 3600);
pipeline.expire(`{session:${sessionId}}:tools`, 3600);
pipeline.expire(`{session:${sessionId}}:cursor`, 3600);
await pipeline.exec();
Strategy 2: Application-level scatter-gather for cross-slot reads
When you must read keys that span slots, issue individual GET commands in parallel using Promise.all. ioredis Cluster routes each command to the correct shard automatically.
// Read sessions from different users — they will be on different shards
const sessionIds = ["abc123", "def456", "ghi789"];
const sessions = await Promise.all(
sessionIds.map((id) => cluster.get(`session:${id}:state`))
);
Lua scripts in cluster mode
ElastiCache rejects Lua scripts (EVAL, EVALSHA) that would access keys across multiple hash slots. All keys touched by the script must be declared in the KEYS array and must hash to the same slot. ioredis uses the first key in KEYS to determine which shard to send the script to.
// Atomic check-and-set using Lua — all keys must share a hash tag
const script = `
local current = redis.call('GET', KEYS[1])
if current == ARGV[1] then
redis.call('SET', KEYS[1], ARGV[2], 'EX', ARGV[3])
return 1
end
return 0
`;
// Both KEYS[1] and the operation use the same key — single slot, safe
const result = await cluster.eval(
script,
1, // numkeys
`{session:${sessionId}}:lock`, // KEYS[1]
expectedValue, // ARGV[1]
newValue, // ARGV[2]
String(ttlSeconds) // ARGV[3]
);
If your Lua script needs to access multiple keys, wrap all of them with the same hash tag to guarantee they land on the same shard.
Pub/sub in cluster mode
Pub/sub in cluster mode has a fundamental limitation: PUBLISH channel message only reaches subscribers on the shard that owns the channel's hash slot — it does not propagate to other shards in the cluster. ioredis handles this by routing SUBSCRIBE to all shards and PUBLISH to the shard that owns the channel slot. For MCP servers using pub/sub for server-push notifications, this means:
- Use hash tags in channel names:
__keyevent@0__:expiredmay arrive on any shard — you need to subscribe on all shards. - For application-level pub/sub (e.g., notifying connected MCP clients of tool result updates), prefer Redis Streams (
XADD/XREAD) over pub/sub — Streams are shard-local and can be reliably consumed from a specific shard. - Alternatively, use a non-cluster ElastiCache replication group (cluster mode disabled) for dedicated pub/sub workloads and a separate cluster-mode group for session state.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
CROSSSLOT Keys in request don't hash to the same slot | Multi-key command (MGET, MSET, DEL key1 key2) where keys hash to different slots | Add hash tags to co-locate related keys: {session:abc}:state and {session:abc}:tools both hash on session:abc; or scatter-gather with individual GET commands |
ERR CROSSSLOT Script attempted to access a non local key | Lua script accesses keys across multiple slots | Ensure all KEYS in the script share the same hash tag; declare all accessed keys in the KEYS array so ioredis can route correctly |
| Connection refused or timeout when using non-cluster ioredis with a CME cluster | Using new Redis({ host, port }) instead of new Redis.Cluster(); CME nodes reject direct connections that don't use the cluster protocol | Switch to new Redis.Cluster([{ host: configEndpoint, port: 6379 }], ...) |
| Stale slot map after scaling event; commands routed to wrong node | ioredis slot map is outdated after ElastiCache adds or removes shards | ioredis auto-refreshes the slot map on MOVED errors; set refreshGroupsIfNoMatch: true in cluster options to aggressively refresh on route misses |
| Pub/sub messages only received on some cluster nodes | PUBLISH only reaches the shard owning the channel slot; subscribers on other shards miss it | Use Redis Streams instead of pub/sub for cross-shard event distribution; or run a separate cluster-mode-disabled replication group for pub/sub |
| Pipeline commands fail with CROSSSLOT | Pipelining commands for keys on different shards; ioredis Cluster splits pipelines by shard, but multi-key commands inside the pipeline still hit the CROSSSLOT restriction | Use single-key commands in pipelines, or ensure all pipelined multi-key commands have the same hash tag; use Promise.all for cross-shard bulk operations |