Guide · AWS ElastiCache

MCP Server ElastiCache Redis — VPC placement, parameter groups, eviction policy, ioredis

AWS ElastiCache Redis is VPC-only — there is no public endpoint. Your MCP server Lambda or ECS task must run inside the same VPC as the ElastiCache replication group, or connect through a VPC peering or Transit Gateway. Three configuration decisions cause the most production incidents: eviction policyallkeys-lru evicts any key under memory pressure, including active sessions, while volatile-lru only evicts keys that have a TTL set, making it the correct policy for MCP session data; reader endpoint vs primary endpoint — point write operations at the primary endpoint and read-heavy status checks at the reader endpoint to balance load across replicas; and parameter groups — ElastiCache does not allow CONFIG SET from your application at runtime, so every Redis parameter (including notify-keyspace-events, maxmemory-policy, and hz) must be configured in a custom parameter group before the cluster is created.

TL;DR

Deploy ElastiCache Redis into private subnets in the same VPC as your MCP server. Create a custom parameter group with maxmemory-policy = volatile-lru so active sessions are never evicted by memory pressure. Connect ioredis to the primary endpoint for writes and the reader endpoint for reads. Enable in-transit encryption and an AUTH token before deployment — these cannot be added post-creation on existing clusters.

VPC and networking architecture

ElastiCache Redis clusters have no public internet endpoint. Every connection must originate from inside the VPC. The access path for an MCP server depends on where it runs:

Create a subnet group that covers the private subnets in each Availability Zone where your MCP server runs. The ElastiCache replication group will place a primary node in one AZ and replica nodes in others. The subnet group controls which AZs are available for node placement.

Security group configuration:

// CDK: ElastiCache replication group in private subnets
import * as elasticache from "aws-cdk-lib/aws-elasticache";
import * as ec2 from "aws-cdk-lib/aws-ec2";

const elasticacheSg = new ec2.SecurityGroup(this, "ElastiCacheSg", {
  vpc,
  description: "ElastiCache Redis — allow access from MCP server only",
  allowAllOutbound: false,
});

const mcpServerSg = new ec2.SecurityGroup(this, "McpServerSg", {
  vpc,
  description: "MCP server security group",
});

// Allow MCP server to reach Redis on port 6379
elasticacheSg.addIngressRule(
  ec2.Peer.securityGroupId(mcpServerSg.securityGroupId),
  ec2.Port.tcp(6379),
  "Allow Redis from MCP server"
);

const subnetGroup = new elasticache.CfnSubnetGroup(this, "RedisSubnetGroup", {
  description: "ElastiCache private subnet group",
  subnetIds: vpc.privateSubnets.map((s) => s.subnetId),
  cacheSubnetGroupName: "mcp-redis-subnet-group",
});

const paramGroup = new elasticache.CfnParameterGroup(this, "RedisParamGroup", {
  cacheParameterGroupFamily: "redis7",
  description: "MCP server Redis parameters",
  properties: {
    "maxmemory-policy": "volatile-lru",
    "hz": "20",
    "lazyfree-lazy-eviction": "yes",
    "lazyfree-lazy-expire": "yes",
    "activerehashing": "yes",
  },
});

const redis = new elasticache.CfnReplicationGroup(this, "McpRedis", {
  replicationGroupDescription: "MCP server session cache",
  numCacheClusters: 2,              // 1 primary + 1 replica
  cacheNodeType: "cache.t4g.small",
  engine: "redis",
  engineVersion: "7.1",
  cacheParameterGroupName: paramGroup.ref,
  cacheSubnetGroupName: subnetGroup.ref,
  securityGroupIds: [elasticacheSg.securityGroupId],
  authToken: process.env.REDIS_AUTH_TOKEN,   // set before creation
  transitEncryptionEnabled: true,
  atRestEncryptionEnabled: true,
  automaticFailoverEnabled: true,
  multiAzEnabled: true,
});

The primaryEndPoint.address attribute of the replication group gives you the primary endpoint; readerEndPoint.address gives the reader endpoint that load-balances across replicas.

Parameter group configuration

ElastiCache does not allow CONFIG SET at runtime — any attempt is rejected with ERR unknown command 'config' or ERR command not allowed from scripts depending on the ElastiCache version. All parameters must be set in a custom parameter group before the cluster is created.

ParameterRecommended valueWhy
maxmemory-policyvolatile-lruEvict only keys that have a TTL set; active session keys without TTL are never evicted under memory pressure. Use allkeys-lru only if you are willing to accept session eviction under load.
hz20Background task frequency (default 10). Higher value = more responsive TTL expiry and lazy-free operations. CPU cost is negligible on modern instances.
lazyfree-lazy-evictionyesFree evicted keys asynchronously in a background thread instead of blocking the event loop. Important when sessions contain large JSON blobs.
lazyfree-lazy-expireyesFree expired keys asynchronously. Reduces jitter during high-TTL expiry bursts.
activerehashingyesIncrementally rehash the main dictionary during idle cycles. Prevents a rehashing pause when the keyspace grows.
notify-keyspace-eventsEx (or empty)Enable keyevent notifications for expiry events only. Set to empty string to disable (default). See the keyspace notifications guide for details.

Connecting with ioredis

Use the primary endpoint for writes (session creation, updates) and the reader endpoint for reads (status checks, session reads that can tolerate a few milliseconds of replication lag).

import Redis from "ioredis";

// Primary endpoint — all writes
const redisWriter = new Redis({
  host: process.env.ELASTICACHE_PRIMARY_ENDPOINT,
  port: 6379,
  password: process.env.REDIS_AUTH_TOKEN,
  tls: {}, // required when transit encryption is enabled
  // ElastiCache uses AWS-managed TLS certificate;
  // the CA is trusted by Node's default bundle since Node 18+
  connectTimeout: 5000,
  maxRetriesPerRequest: 3,
  retryStrategy: (times: number) => Math.min(times * 100, 3000),
  lazyConnect: false,
});

// Reader endpoint — read-only operations
const redisReader = new Redis({
  host: process.env.ELASTICACHE_READER_ENDPOINT,
  port: 6379,
  password: process.env.REDIS_AUTH_TOKEN,
  tls: {},
  connectTimeout: 5000,
  maxRetriesPerRequest: 3,
  retryStrategy: (times: number) => Math.min(times * 100, 3000),
  lazyConnect: false,
});

// Store a session with TTL
export async function setSession(
  sessionId: string,
  data: Record,
  ttlSeconds: number
): Promise {
  // SET with EX ensures the key has a TTL — required for volatile-lru to protect it
  await redisWriter.set(
    `session:${sessionId}`,
    JSON.stringify(data),
    "EX",
    ttlSeconds
  );
}

// Read a session (can use reader replica)
export async function getSession(
  sessionId: string
): Promise | null> {
  const raw = await redisReader.get(`session:${sessionId}`);
  if (!raw) return null;
  return JSON.parse(raw) as Record;
}

Always set a TTL when storing session data. Without a TTL, the key does not qualify for volatile-lru eviction and will remain in memory indefinitely, eventually causing out-of-memory errors.

Eviction policy selection

The maxmemory-policy parameter controls what Redis does when it reaches the memory limit. For MCP session state, volatile-lru is almost always the correct choice.

PolicyWhat gets evictedBest for MCP
noevictionNothing — writes fail with OOM error when memory is fullOnly if you want explicit control and are certain capacity is always sufficient; MCP writes will fail with errors under memory pressure
allkeys-lruAny key, TTL or not, least-recently-used firstCache-only workloads; not for session state — active sessions without TTL can be evicted, causing unexpected session loss
volatile-lruOnly keys with a TTL set, least-recently-used firstCorrect for MCP session state — always set TTL on session keys; long-lived configuration keys without TTL are protected from eviction
allkeys-lfuAny key, least-frequently-used firstCache workloads with skewed access patterns; not for session state
volatile-ttlKeys with TTL set, shortest-TTL firstSession data where you want to prefer evicting near-expiry sessions; acceptable alternative to volatile-lru

Set TTL on every session key with EX in the SET command or EXPIRE. If you store a session key without a TTL, it will never be evicted under volatile-lru, accumulating until memory is exhausted.

Common failure modes

SymptomCauseFix
Connection timeout from Lambda or ECS taskLambda not in VPC, or in a different VPC from ElastiCache; or security group does not allow TCP 6379 from the app security groupEnsure Lambda has vpcConfig pointing to the same VPC; add inbound rule on ElastiCache SG allowing TCP 6379 from the MCP server SG
NOAUTH Authentication requiredAUTH token is enabled on the cluster but ioredis password option is not setPass password: process.env.REDIS_AUTH_TOKEN in the ioredis constructor
TLS handshake error on connectiontransitEncryptionEnabled: true requires TLS on the client; ioredis tls: {} option missingAdd tls: {} to the ioredis constructor; if you see certificate errors, verify the Node.js version supports AWS's root CA (Node 18+ does by default)
Sessions unexpectedly disappearing under loadmaxmemory-policy = allkeys-lru — active sessions being evicted as memory fillsSwitch to volatile-lru in the parameter group; ensure all session keys are stored with EX ttl so they are eligible for managed eviction at expiry, not surprise eviction
ERR command not allowed or ERR unknown command 'config'Application is trying to run CONFIG SET at runtime; ElastiCache blocks this commandMove all Redis configuration to the parameter group; delete the CONFIG SET calls from application startup code
Writes going to the reader endpoint, causing READONLY errorsApplication is writing to the reader endpoint URL instead of the primary endpointSeparate write and read connections: use ELASTICACHE_PRIMARY_ENDPOINT for writes, ELASTICACHE_READER_ENDPOINT for reads; the reader endpoint round-robins across replicas which are read-only