ElastiCache & Caching Strategies
A comprehensive deep dive into Amazon ElastiCache — Redis vs Memcached, all caching strategies, Redis data structures, cluster modes, replication, persistence, eviction policies, and common caching patterns for the DVA-C02 exam.
What is Amazon ElastiCache?
Amazon ElastiCache is a fully managed in-memory data store service that supports Redis and Memcached. It sits in front of your database to absorb read traffic, reduce latency from milliseconds to microseconds, and dramatically lower database load.
Core mental model: Your database is slow (disk I/O). Your cache is fast (RAM). Put frequently-read, rarely-changed data in the cache. The application checks cache first — on a hit, skip the database entirely.
When to use ElastiCache:
- Read-heavy workloads with repetitive queries
- Session storage across stateless application instances
- Real-time leaderboards, rate limiting, pub/sub
- Reducing RDS/DynamoDB read load and cost
Redis vs Memcached
| Feature | Redis | Memcached |
|---|---|---|
| Data structures | Strings, Lists, Sets, Sorted Sets, Hashes, Bitmaps, HyperLogLog, Streams | Strings only |
| Persistence | ✅ RDB snapshots + AOF log | ❌ |
| Replication | ✅ Primary + up to 5 replicas | ❌ |
| Multi-AZ failover | ✅ Automatic | ❌ |
| Cluster mode (horizontal scale) | ✅ Up to 500 nodes | ✅ Multi-threaded sharding |
| Pub/Sub | ✅ | ❌ |
| Lua scripting | ✅ | ❌ |
| Transactions | ✅ MULTI/EXEC | ❌ |
| Sorted sets / leaderboards | ✅ | ❌ |
| Multi-threaded | ❌ (single-threaded) | ✅ |
| DVA-C02 answer | Almost always | Only when multi-threading or pure simplicity is asked |
Exam rule: Any question involving persistence, replication, Multi-AZ, pub/sub, sorted sets, or sessions → Redis. Simple pure key-value cache with multi-threading → Memcached.
Caching Strategies
1. Lazy Loading (Cache-Aside)
The most common pattern. Check cache first; only fetch from DB on a miss, then populate the cache.
1import { createClient } from 'redis';
2const redis = createClient({ url: process.env.REDIS_URL });
3
4async function getUser(userId) {
5 const cacheKey = `user:${userId}`;
6
7 // 1. Check cache
8 const cached = await redis.get(cacheKey);
9 if (cached) {
10 return JSON.parse(cached); // cache HIT — database not touched
11 }
12
13 // 2. Cache MISS — fetch from database
14 const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
15
16 // 3. Populate cache with TTL
17 await redis.setEx(cacheKey, 300, JSON.stringify(user)); // expires in 5 min
18
19 return user;
20}| Pros | Cons |
|---|---|
| Only caches data that is actually requested | First request after miss is slow (cache stampede risk) |
| Cache failure doesn't break the app | Stale data until TTL expires |
| Works well with any data store | Requires explicit cache invalidation on updates |
Cache stampede (thundering herd): When many requests simultaneously get a cache miss and all hit the database. Fix with a short lock or probabilistic early expiration.
2. Write-Through
Write to cache and database simultaneously on every write. Cache is always up to date.
1async function updateUser(userId, data) {
2 // Write to DB first
3 await db.query('UPDATE users SET ... WHERE id = $1', [userId]);
4
5 // Immediately update cache
6 const cacheKey = `user:${userId}`;
7 await redis.setEx(cacheKey, 3600, JSON.stringify(data));
8}| Pros | Cons |
|---|---|
| Cache always consistent with DB | Write penalty — every write hits both DB and cache |
| No stale data | Cache filled with data that may never be read |
| No explicit invalidation needed | Node failure before DB write = data loss |
3. Write-Behind (Write-Back)
Write to cache immediately; asynchronously flush to the database. Lowest write latency.
1async function updateUserWriteBehind(userId, data) {
2 const cacheKey = `user:${userId}`;
3
4 // Instant cache write — return immediately
5 await redis.setEx(cacheKey, 3600, JSON.stringify(data));
6
7 // Queue async DB write (processed by a background worker)
8 await redis.lPush('db:write:queue', JSON.stringify({ userId, data, ts: Date.now() }));
9}| Pros | Cons |
|---|---|
| Lowest write latency | Risk of data loss if cache crashes before DB write |
| Database load reduced | Complex to implement correctly |
| Handles write bursts gracefully | Eventual consistency — DB may lag behind |
4. Read-Through
Cache sits in front of DB. On a miss, the cache itself fetches from the DB (not the application). Application only talks to the cache.
| Pros | Cons |
|---|---|
| Simpler application code | Cache must support read-through (not all do) |
| Consistent caching logic | First-request latency on cold start |
5. TTL (Time-To-Live)
Set an expiry on every cached key. After expiry, the key is deleted and the next request triggers a cache miss + DB fetch.
1// TTL strategies
2await redis.setEx('product:123', 60, JSON.stringify(product)); // 1 min — frequently changing
3await redis.setEx('config:flags', 3600, JSON.stringify(flags)); // 1 hour — rarely changing
4await redis.setEx('user:session', 86400, JSON.stringify(session)); // 1 day — session
5
6// Always combine TTL with Lazy Loading
7// TTL prevents stale data from living foreverCache Invalidation
The hardest problem in computer science. When data changes in the DB, the cache entry must be removed or updated.
1async function deleteProduct(productId) {
2 await db.query('DELETE FROM products WHERE id = $1', [productId]);
3
4 // Explicitly invalidate all related cache keys
5 await redis.del(`product:${productId}`);
6 await redis.del('products:list:all');
7 await redis.del('products:list:featured');
8}Strategies:
- Delete on write — remove the key; next read fetches from DB (lazy loading)
- Update on write — overwrite with new value (write-through)
- TTL expiry — let it expire naturally (acceptable for low-freshness requirements)
Redis Data Structures
Redis is not just a key-value store — it has rich data types, each optimised for specific use cases.
Strings
1await redis.set('counter', 0);
2await redis.incr('counter'); // atomic increment → 1
3await redis.incrBy('counter', 5); // → 6
4await redis.setEx('session:abc', 3600, token); // with TTLHashes — Object Storage
1// Store a user object field-by-field (memory efficient)
2await redis.hSet('user:123', {
3 name: 'Alice',
4 email: 'alice@example.com',
5 tier: 'premium',
6});
7
8const name = await redis.hGet('user:123', 'name');
9const user = await redis.hGetAll('user:123'); // returns full objectLists — Queues and Activity Feeds
1// Activity feed (newest first)
2await redis.lPush('feed:usr_01', JSON.stringify({ action: 'liked', ts: Date.now() }));
3await redis.lTrim('feed:usr_01', 0, 99); // keep only 100 most recent items
4
5const feed = await redis.lRange('feed:usr_01', 0, 19); // get first 20
6
7// Simple queue (FIFO)
8await redis.rPush('job:queue', JSON.stringify(job)); // enqueue
9const job = await redis.lPop('job:queue'); // dequeueSorted Sets — Leaderboards and Rankings
The most powerful Redis data structure. Each member has a score; the set is always sorted by score.
1// Game leaderboard
2await redis.zAdd('leaderboard:global', [
3 { score: 9850, value: 'player:alice' },
4 { score: 8200, value: 'player:bob' },
5 { score: 7500, value: 'player:carol' },
6]);
7
8// Update a player's score
9await redis.zIncrBy('leaderboard:global', 150, 'player:bob'); // bob → 8350
10
11// Get top 10 (highest scores first)
12const top10 = await redis.zRangeWithScores('leaderboard:global', 0, 9, { REV: true });
13
14// Get a player's rank (0-indexed, 0 = top)
15const rank = await redis.zRevRank('leaderboard:global', 'player:alice');Sets — Unique Collections and Relationships
1// Track unique page visitors (no duplicates)
2await redis.sAdd(`visitors:${today}`, userId);
3const uniqueCount = await redis.sCard(`visitors:${today}`);
4
5// Common friends between two users
6const commonFriends = await redis.sInter('friends:alice', 'friends:bob');
7
8// Check membership O(1)
9const isFollowing = await redis.sIsMember('follows:alice', 'bob');Session Store Pattern
The most common ElastiCache use case in the DVA-C02 exam. Store sessions in Redis so any instance in your fleet can serve any user — enabling horizontal scaling.
1// Store session
2const sessionId = crypto.randomUUID();
3await redis.setEx(
4 `session:${sessionId}`,
5 86400, // 24 hour TTL
6 JSON.stringify({ userId, email, tier, loginAt: Date.now() })
7);
8
9// Read session (any instance)
10const session = JSON.parse(await redis.get(`session:${sessionId}`) ?? 'null');
11if (!session) throw new Error('Session expired');
12
13// Extend session on activity (sliding expiry)
14await redis.expire(`session:${sessionId}`, 86400);Rate Limiting Pattern
Redis atomic operations make it ideal for distributed rate limiting.
1async function checkRateLimit(userId, limitPerMinute = 100) {
2 const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`; // per-minute window
3
4 const count = await redis.incr(key);
5 if (count === 1) {
6 await redis.expire(key, 60); // set TTL on first request
7 }
8
9 if (count > limitPerMinute) {
10 throw new Error('Rate limit exceeded'); // return HTTP 429
11 }
12
13 return { remaining: limitPerMinute - count };
14}Redis Cluster Modes
Cluster Mode Disabled (Single Shard)
One primary node + up to 5 read replicas. All data fits in one shard. Multi-AZ with automatic failover.
- Failover: If primary fails, a replica is promoted automatically (Multi-AZ must be enabled)
- Scaling reads: Add replicas (up to 5)
- Scaling writes: Increase node size (scale up) — no horizontal write scaling
- Max data: Limited to single node memory
Cluster Mode Enabled (Multiple Shards)
Data is partitioned across multiple shards using consistent hashing. Each shard has its own primary + replicas.
- 16,384 hash slots distributed across shards
- Scale writes by adding shards
- Up to 500 nodes (250 shards × 2 nodes each, or 90 shards × 6 nodes each)
- Online resharding — add/remove shards without downtime
| Feature | Cluster Disabled | Cluster Enabled |
|---|---|---|
| Shards | 1 | Up to 500 |
| Scale writes | ❌ (scale up only) | ✅ (add shards) |
| Scale reads | ✅ (add replicas) | ✅ |
| Max nodes | 6 (1+5) | 500 |
| Multi-key ops | ✅ | ❌ (keys must be on same shard) |
| Failover | ✅ | ✅ per shard |
Redis Persistence
RDB (Redis Database Backup)
Point-in-time snapshots saved to disk at configurable intervals.
1save 900 1 # snapshot if 1 key changed in 900s
2save 300 10 # snapshot if 10 keys changed in 300s
3save 60 10000 # snapshot if 10,000 keys changed in 60s- Pros: Compact file, fast restart, minimal performance impact
- Cons: Data loss between snapshots (up to the last save interval)
AOF (Append-Only File)
Logs every write operation. On restart, Redis replays the log to reconstruct state.
| AOF fsync policy | Durability | Performance |
|---|---|---|
always | Every write persisted | Slowest |
everysec | Max 1 second of data loss | Good balance (recommended) |
no | OS decides when to flush | Fastest, least durable |
- Pros: Up to 1 second of data loss with
everysec - Cons: Larger file than RDB, slower restart
Best practice: Enable both RDB + AOF. Use RDB for fast restarts; AOF for durability. ElastiCache Redis supports AOF with
appendonly yes.
Cache Eviction Policies
When the cache is full and a new key must be written, Redis evicts an existing key based on the configured policy:
| Policy | Behaviour | Use case |
|---|---|---|
noeviction | Return error on write when full | Never use for cache |
allkeys-lru | Evict least recently used key (any key) | Most common for cache |
volatile-lru | Evict LRU key with TTL set | Mixed cache + persistent data |
allkeys-lfu | Evict least frequently used key | Skewed access patterns |
volatile-lfu | Evict LFU key with TTL set | Mixed with TTL keys |
allkeys-random | Evict random key | Uniform access pattern |
volatile-random | Evict random key with TTL | Rarely useful |
volatile-ttl | Evict key with shortest TTL | Expire-prioritised eviction |
Security
1# Redis AUTH token (password)
2aws elasticache create-replication-group \
3 --replication-group-id my-redis \
4 --auth-token "MyStrongPassword123!" \
5 --transit-encryption-enabled # TLS in transit
6 --at-rest-encryption-enabled # encrypted at rest (KMS)
7
8# IAM auth (Redis 7.0+) — use AWS credentials instead of password
9aws elasticache create-user \
10 --user-id app-user \
11 --user-name app-user \
12 --engine redis \
13 --authentication-type iamElastiCache is deployed inside a VPC. Access is controlled via Security Groups — no public internet access by default.
DVA-C02 Quick Reference
| Topic | Key Fact |
|---|---|
| Best caching strategy | Lazy Loading + TTL (most flexible) |
| Always fresh cache | Write-Through |
| Lowest write latency | Write-Behind |
| Persistence support | Redis only (not Memcached) |
| Replication + Multi-AZ | Redis only |
| Pub/Sub support | Redis only |
| Sorted sets / leaderboards | Redis only |
| Multi-threaded | Memcached |
| Session storage | Redis (shared across fleet) |
| Cluster disabled max replicas | 5 read replicas |
| Cluster enabled max nodes | 500 |
| Hash slots | 16,384 |
| Scale writes | Cluster Mode Enabled (add shards) |
| RDB vs AOF | RDB = snapshots; AOF = operation log |
| Most common eviction policy | allkeys-lru |
| Rate limiting tool | Redis INCR + EXPIRE |
| Cache always in | VPC — no public access |
Practice Questions3
Q1. A developer is choosing between ElastiCache for Redis and ElastiCache for Memcached for a session store that must persist data across node restarts. Which should be chosen and why?
Select one answer before revealing.
Q2. A developer implements a caching layer using ElastiCache for Redis. When a cache miss occurs, the application queries the database and writes the result to the cache. What caching strategy is this?
Select one answer before revealing.
Q3. A developer uses ElastiCache for Redis to cache database query results. After deploying a new version of the application that uses different query logic, stale data is returned. Which approach correctly invalidates affected cache entries?
Select one answer before revealing.