Amazon ElastiCache
Engine-level Redis depth (data structures, persistence, clustering internals, distributed locks, cache-stampede mitigation) lives in backend/09_caching/redis/. Operational depth (failover, Serverless, parameter groups) is in 02_failover_serverless_operations.md. This file is the AWS-managed-service overview.
Managed Redis / Memcached. Saves you from operating a cache cluster. Two engines:
- ElastiCache for Redis / Valkey — feature-rich; pub/sub, sorted sets, persistence, streams.
- ElastiCache for Memcached — multi-threaded simple KV; rarely the right choice in 2024+.
Almost always: Redis (or Valkey, the open-source AWS-backed fork after Redis Inc.’s license change).
What ElastiCache gives you
- Provisioning, patching, monitoring.
- Multi-AZ replication with automatic failover.
- Backup/restore (RDB snapshots).
- Encryption in transit (TLS) and at rest.
- IAM-based authentication.
For Redis fundamentals — data structures, eviction, caching patterns — see backend/09_caching/redis/.
Deployment topologies
Single node
A single primary, no replica. Cheap, no HA. Use only for dev/staging.
Replication group (cluster mode disabled)
One primary + up to 5 replicas. Automatic failover. Single primary — write throughput limited by one node. Reads can scale across replicas.
Cluster mode enabled
Sharded. Data partitioned across N “node groups” (shards), each with primary + replicas. Hash slots distribute keys. Use when you outgrow a single primary’s write throughput.
# Cluster mode requires a cluster-aware client
from redis.cluster import RedisCluster
r = RedisCluster(host="prod.xxx.use1.cache.amazonaws.com", port=6379, ssl=True)
Trade-off: cluster mode complicates multi-key operations. MGET / pipelines across multiple slots need extra logic; transactions can’t span slots.
Endpoints
| Endpoint | Purpose |
|---|---|
| Primary | writes |
| Reader | round-robin across replicas (cluster-mode-disabled) |
| Configuration | cluster topology discovery (cluster-mode-enabled) |
| Node | direct to a specific node |
# cluster-mode-disabled — separate read/write endpoints
write = redis.Redis(host="prod.xxx.use1.cache.amazonaws.com", port=6379, ssl=True)
read = redis.Redis(host="prod-ro.xxx.use1.cache.amazonaws.com", port=6379, ssl=True)
Versioning and feature flags
ElastiCache lags upstream Redis by months. Check the version map before relying on a specific feature (Redis Streams, JSON, RedisGraph, etc.).
Newer choice: ElastiCache Serverless (2023+). No node sizing — usage-based pricing per byte stored + per request. Good for unpredictable load.
Sizing
Memory is king. Watch:
BytesUsedForCache— actual data size.DatabaseMemoryUsagePercentage— running close to 100% leads to evictions.Evictions— keys being dropped because the cache is full.CurrConnections— Redis has a default limit of 65k; each app pod opens N.
CPU rarely matters except for Lua scripts or KEYS * in production (don’t).
Persistence options
- RDB snapshots — point-in-time dump. Daily by default; restore creates a fresh cluster from a snapshot.
- AOF — append-only log of every write. ElastiCache exposes limited AOF — usually you’d use snapshots.
If your cache is only a cache (rebuildable from source of truth), don’t persist. If it’s a primary store (rate-limit counters, sessions), persist + replicate + back up.
Connection patterns for Python
import redis
from redis.connection import SSLConnection
# Cluster-mode-disabled
pool = redis.ConnectionPool(
host="prod.xxx.use1.cache.amazonaws.com",
port=6379,
connection_class=SSLConnection,
max_connections=50,
decode_responses=True,
)
r = redis.Redis(connection_pool=pool)
For cluster mode, use redis.cluster.RedisCluster (sync) or redis.asyncio.cluster.RedisCluster (async, redis-py 5+).
Pool sizing on k8s
N pods × max_connections per pod = total. ElastiCache caps at 65k. With 100 pods at 100 connections each you’re at 10k — fine. With 10000 lambdas? You need an in-front proxy or fewer connections per worker.
Auth and encryption
aws elasticache create-replication-group \
--replication-group-id orders \
--engine redis \
--transit-encryption-enabled \
--at-rest-encryption-enabled \
--auth-token "long-random-string" \
...
Client supplies the auth token as password. IAM auth (newer) replaces shared tokens with short-lived IAM-derived tokens — same model as RDS IAM auth.
Cache-aside is the dominant pattern
def get_user(user_id: int) -> User:
key = f"user:{user_id}"
raw = r.get(key)
if raw:
return User.parse_raw(raw)
user = db.query(User).get(user_id)
if user:
r.setex(key, 3600, user.json()) # 1h TTL
return user
Plus invalidation on write:
def update_user(user_id: int, **fields):
db.update_user(user_id, **fields)
r.delete(f"user:{user_id}") # invalidate
Caveats:
- Cache stampede when a hot key expires — see backend/09_caching/redis/.
- Stale on race — concurrent update + read can re-cache stale data. Mitigate with write-through or short TTLs.
Common gotchas
KEYS *in production. Blocks the single-threaded server. UseSCAN.- Hot key (sharded mode). All traffic for one key hits one shard. Replicate the value to N suffixed keys.
- TLS without
ssl=Truein client. Connections refused. Easy to miss in dev → prod transition. - Memcached vs Redis interchangeability. Memcached is just a key/value cache — no pub/sub, no sorted sets, no streams, no persistence. Don’t pick it unless you know why.
- Default
maxmemory-policy: noevictionin ElastiCache. Hitting the limit returns errors instead of evicting. Set toallkeys-lrufor a generic cache. - Snapshot restore creates a NEW cluster. No in-place rollback.
Cost optimization
- Reserved nodes for steady workloads (~50% off).
- Right-size memory —
cache.r6g.large(13GB) costs 4×cache.t4g.small(1.5GB); a too-large instance with 10% memory used is waste. - Serverless for spiky / dev / staging — pay per byte + request.
- Move to Valkey — same engine post-fork, AWS bills it slightly differently; some price savings appearing.
Common interview pattern: sessions / rate limit
# Rate limit: max 100 requests per minute per user
def check_rate(user_id: int) -> bool:
key = f"rate:{user_id}:{int(time.time() // 60)}"
count = r.incr(key)
if count == 1:
r.expire(key, 60)
return count <= 100
# Session token
r.setex(f"sess:{token}", 3600, json.dumps(session_data))
Interview angle
- “ElastiCache Redis vs Memcached?” — Memcached is a simple multi-threaded KV; no persistence, no replication, no rich types. Redis is feature-rich (sorted sets, streams, pub/sub, persistence, replication). Pick Redis unless you know your workload is hot-CPU-bound on simple KV with no need for richness.
- “Cluster mode enabled vs disabled?” — disabled: single primary + replicas; reads scale, writes don’t. Enabled: sharded across N node groups; writes scale; multi-key ops constrained to same hash slot.
- “How do you handle a cache stampede?” — single-flight (one request rebuilds, others wait), probabilistic early expiration (refresh near TTL), or pre-warm. Avoid: thousands of requests hitting the DB simultaneously when one hot key expires.
- “What happens if ElastiCache fails?” — depends on your app design. As a pure cache: DB takes the load, latency spikes. As a primary store (sessions, rate limits): partial outage. Plan for: short TTLs, soft-fail on cache errors, sufficient DB capacity to absorb cache loss.
- “How do you authenticate to ElastiCache?” — AUTH token (shared password), or IAM auth (newer, short-lived tokens via SDK). Both with TLS in transit; at-rest encryption is a checkbox.
- “Serverless ElastiCache — when?” — unpredictable / spiky workloads (you’d over-provision otherwise); dev/staging (avoid paying for idle nodes); apps that scale fast (no manual capacity changes needed).