ElastiCache
ElastiCache is a managed in-memory data store running Redis or Memcached. It sits between your application and your database, serving frequently accessed data at sub-millisecond latency.
Why Caching?
Section titled “Why Caching?”Without cache:Client ──► App ──► Database (10–100ms)
With cache:Client ──► App ──► Cache (< 1ms, hit) ──► return └──► Cache miss ──► Database (10–100ms) ──► store in cache ──► returnCaching reduces database load, lowers latency, and improves throughput. A cache hit is typically 100x faster than a database query.
Redis vs Memcached
Section titled “Redis vs Memcached”| Feature | Redis | Memcached |
|---|---|---|
| Data structures | Strings, hashes, lists, sets, sorted sets, streams, bitmaps | Strings only |
| Persistence | Yes (snapshots + AOF) | No (in-memory only) |
| Replication | Yes (primary + replicas) | No |
| Cluster mode | Yes (automatic sharding) | Yes (client-side sharding) |
| Pub/Sub | Yes | No |
| Transactions | Yes (MULTI/EXEC) | No |
| Lua scripting | Yes | No |
| Use cases | Caching, sessions, leaderboards, queues, pub/sub, real-time analytics | Simple key-value caching |
Choose Redis for most use cases — it’s more versatile. Choose Memcached only for simple caching with multi-threaded performance.
ElastiCache for Redis
Section titled “ElastiCache for Redis”Cluster Modes
Section titled “Cluster Modes”Cluster Mode Disabled (Single Shard):
ReadClient ──► Primary ──────► Replica 1 (AZ-a) (write) ──────► Replica 2 (AZ-b)- One primary node (writes) + up to 5 replicas (reads).
- All nodes hold the full dataset.
- Good for: datasets that fit in one node (up to ~340 GB).
Cluster Mode Enabled (Multiple Shards):
Client ──► Shard 1: Primary + Replicas (keys A–M) ──► Shard 2: Primary + Replicas (keys N–Z) ──► Shard 3: Primary + Replicas (keys ...etc)- Data is partitioned across shards (each shard is a primary + replicas).
- Up to 500 shards.
- Good for: large datasets, high write throughput (writes scale with shards).
Node Types
Section titled “Node Types”| Type | vCPUs | Memory | Use Case |
|---|---|---|---|
cache.t3.micro | 2 | 0.5 GB | Dev/test (free tier eligible) |
cache.t3.medium | 2 | 3.09 GB | Small production |
cache.r6g.large | 2 | 13.07 GB | Production caching |
cache.r6g.xlarge | 4 | 26.32 GB | Large datasets |
cache.r6g.4xlarge | 16 | 105.81 GB | High-memory workloads |
Graviton (r6g, r7g) nodes are ~20% cheaper than Intel equivalents.
Creating a Redis Cluster
Section titled “Creating a Redis Cluster”# Cluster mode disabled (1 primary + 2 replicas)aws elasticache create-replication-group \ --replication-group-id my-redis \ --replication-group-description "Production cache" \ --engine redis \ --engine-version 7.0 \ --node-type cache.r6g.large \ --num-cache-clusters 3 \ --automatic-failover-enabled \ --multi-az-enabled \ --at-rest-encryption-enabled \ --transit-encryption-enabled \ --cache-subnet-group-name my-cache-subnets \ --security-group-ids sg-redisCaching Patterns
Section titled “Caching Patterns”Cache-Aside (Lazy Loading)
Section titled “Cache-Aside (Lazy Loading)”The most common pattern — the application manages the cache:
import redis, json
r = redis.Redis(host='my-redis.abc.ng.0001.use1.cache.amazonaws.com', port=6379)
def get_user(user_id): # 1. Check cache cached = r.get(f"user:{user_id}") if cached: return json.loads(cached) # cache hit
# 2. Cache miss — query database user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. Store in cache with TTL r.setex(f"user:{user_id}", 3600, json.dumps(user)) # 1 hour TTL
return user| Pros | Cons |
|---|---|
| Only caches data that’s actually requested | First request for each key is slow (cache miss) |
| Cache failures don’t break the app | Data can become stale (until TTL expires) |
| Simple to implement | Application must handle both cache and DB |
Write-Through
Section titled “Write-Through”Write to cache and database at the same time:
def update_user(user_id, data): # Write to DB db.execute("UPDATE users SET ... WHERE id = %s", user_id, data)
# Write to cache r.setex(f"user:{user_id}", 3600, json.dumps(data))| Pros | Cons |
|---|---|
| Cache is always up to date | Every write hits both cache and DB (slower writes) |
| No stale data | Cache fills with data that may never be read |
Write-Behind (Write-Back)
Section titled “Write-Behind (Write-Back)”Write to cache first, then asynchronously write to the database:
App ──write──► Cache ──async──► DatabaseVery fast writes, but risk of data loss if the cache fails before the DB write. Rarely used with ElastiCache — more common with specialized caching layers.
Cache Invalidation
Section titled “Cache Invalidation”The hardest part of caching. Strategies:
| Strategy | How It Works | Trade-off |
|---|---|---|
| TTL-based | Cache expires after N seconds | Simple; data can be stale for up to TTL |
| Event-based | Delete cache when data changes | Consistent; requires invalidation logic |
| Version keys | user:123:v5 — increment version on change | No delete needed; old versions expire via TTL |
Common Use Cases
Section titled “Common Use Cases”Session Storage
Section titled “Session Storage”# Store session in Redis (instead of server memory)r.setex(f"session:{session_id}", 1800, json.dumps({ 'user_id': 'user_123', 'role': 'admin', 'login_time': '2026-02-16T10:00:00Z'}))
# Any app server can read the sessionsession = json.loads(r.get(f"session:{session_id}"))Redis-backed sessions work across multiple app servers — critical for load-balanced applications.
Leaderboard (Sorted Sets)
Section titled “Leaderboard (Sorted Sets)”# Add scoresr.zadd("leaderboard", {"alice": 950, "bob": 870, "charlie": 1020})
# Top 10 playerstop_10 = r.zrevrange("leaderboard", 0, 9, withscores=True)# [('charlie', 1020), ('alice', 950), ('bob', 870)]
# Player's rankrank = r.zrevrank("leaderboard", "alice") # 1 (0-indexed)Sorted sets maintain order automatically — O(log N) for inserts and rank lookups.
Rate Limiting
Section titled “Rate Limiting”def is_rate_limited(user_id, limit=100, window=60): key = f"ratelimit:{user_id}" current = r.incr(key) if current == 1: r.expire(key, window) # set TTL on first request return current > limitPub/Sub (Real-Time Notifications)
Section titled “Pub/Sub (Real-Time Notifications)”# Publisherr.publish("notifications", json.dumps({ 'user_id': 'user_123', 'message': 'Your order has shipped'}))
# Subscriberpubsub = r.pubsub()pubsub.subscribe("notifications")for message in pubsub.listen(): if message['type'] == 'message': data = json.loads(message['data']) send_push_notification(data)ElastiCache Security
Section titled “ElastiCache Security”| Practice | How |
|---|---|
| Network isolation | Place in private subnets. Allow only app security groups. |
| Encryption at rest | Enable at-rest-encryption-enabled (KMS) |
| Encryption in transit | Enable transit-encryption-enabled (TLS) |
| Auth | Enable Redis AUTH token (password) or IAM authentication |
| No public access | ElastiCache is VPC-only — no public endpoints |
ElastiCache vs DynamoDB DAX
Section titled “ElastiCache vs DynamoDB DAX”| ElastiCache (Redis) | DynamoDB DAX | |
|---|---|---|
| Type | General-purpose cache | DynamoDB-specific cache |
| Manages | Your caching logic | Transparent (no code changes) |
| Data source | Any (RDS, APIs, DynamoDB, etc.) | DynamoDB only |
| Use case | Flexible caching, sessions, leaderboards | Accelerate DynamoDB reads |
If you only need to cache DynamoDB reads, DAX is simpler. For everything else, use ElastiCache.
Key Takeaways
Section titled “Key Takeaways”- ElastiCache Redis is a managed in-memory store for caching, sessions, leaderboards, rate limiting, and pub/sub.
- Cache-aside is the default pattern: check cache → miss → query DB → store in cache.
- Set TTLs on all cached data to prevent staleness. Event-based invalidation for consistency-critical data.
- Use cluster mode enabled for large datasets and high write throughput. Use cluster mode disabled for simpler setups.
- Place ElastiCache in private subnets with encryption at rest and in transit.
- Choose Redis over Memcached in almost all cases — richer data structures, persistence, replication.