Episode 3 — NodeJS MongoDB Backend Architecture / 3.16 — Caching with Redis
3.16 — Quick Revision: Caching with Redis
A concise cheat sheet covering Redis data types, essential commands, caching patterns, Pub/Sub, TTL management, and common integration patterns for rapid review.
<< Previous: Interview Questions | Next Section: 3.17 — Error Handling >>
Caching Patterns
CACHE-ASIDE (Lazy Loading)
1. Check cache → HIT? Return cached data
2. Cache MISS → Query database
3. Store in cache → SET key value EX ttl
4. Return data
WRITE-THROUGH
1. Write to DB → Update database
2. Write to cache → SET updated data in cache
(Cache always in sync, but writes are slower)
WRITE-BEHIND
1. Write to cache → Fast, return immediately
2. Async write to DB → Background job writes to database later
(Fast writes, risk of data loss)
Redis Data Types
| Type | Commands | Use Case | Example |
|---|---|---|---|
| String | SET, GET, INCR, MSET, MGET | Caching, counters, locks | SET user:name "Alice" EX 3600 |
| Hash | HSET, HGET, HGETALL, HINCRBY | Objects, profiles | HSET user:1 name "Alice" age "30" |
| List | LPUSH, RPUSH, LPOP, RPOP, LRANGE | Queues, feeds | LPUSH queue:jobs "job1" |
| Set | SADD, SMEMBERS, SINTER, SUNION | Unique tracking, tags | SADD online-users "user:1" |
| Sorted Set | ZADD, ZRANGE, ZREVRANGE, ZRANK | Leaderboards, rankings | ZADD scores 100 "Alice" |
Essential Redis Commands
KEY MANAGEMENT
SET key value [EX sec] [NX] Store a value (with optional TTL, only-if-new)
GET key Retrieve a value
DEL key [key ...] Delete key(s)
EXISTS key Check if key exists (1/0)
TYPE key Get data type of key
RENAME old new Rename a key
TTL MANAGEMENT
EXPIRE key seconds Set TTL on existing key
PEXPIRE key milliseconds Set TTL in milliseconds
TTL key Get remaining TTL (-1=no expiry, -2=not found)
PTTL key Get remaining TTL in milliseconds
PERSIST key Remove TTL (make permanent)
STRINGS
SET key value Store string
GET key Retrieve string
INCR key / DECR key Atomic increment/decrement by 1
INCRBY key n / DECRBY key n Atomic increment/decrement by n
MSET k1 v1 k2 v2 Set multiple keys
MGET k1 k2 Get multiple keys
SETNX key value Set only if NOT exists
HASHES
HSET key field value [f v] Set field(s) in hash
HGET key field Get single field
HGETALL key Get all fields and values
HDEL key field Delete a field
HEXISTS key field Check field exists
HINCRBY key field n Increment numeric field
HKEYS key / HVALS key Get all field names / values
HLEN key Count fields
LISTS
LPUSH key val [val ...] Push to head
RPUSH key val [val ...] Push to tail
LPOP key / RPOP key Pop from head / tail
LRANGE key start stop Get range (0 -1 = all)
LLEN key Get length
LTRIM key start stop Keep only range, remove rest
BLPOP key timeout Blocking pop (for queues)
SETS
SADD key member [member ...] Add member(s)
SMEMBERS key Get all members
SISMEMBER key member Check membership (1/0)
SCARD key Count members
SREM key member Remove member
SINTER key1 key2 Intersection
SUNION key1 key2 Union
SDIFF key1 key2 Difference
SORTED SETS
ZADD key score member [...] Add with score
ZRANGE key start stop [REV] Get by rank (ascending/descending)
ZRANGEBYSCORE key min max Get by score range
ZSCORE key member Get member's score
ZRANK key member Get rank (ascending, 0-based)
ZREVRANK key member Get rank (descending, 0-based)
ZINCRBY key n member Increment score
ZCARD key Count members
ZREM key member Remove member
SERVER
PING Test connection (returns PONG)
INFO [section] Server info and stats
DBSIZE Number of keys
FLUSHDB Delete all keys in current DB
SCAN cursor [MATCH pat] Iterate keys (production-safe)
MONITOR Watch all commands in real time
node-redis v4+ Cheat Sheet
// SETUP
const { createClient } = require('redis');
const client = createClient({ url: 'redis://localhost:6379' });
client.on('error', (err) => console.error('Redis Error:', err));
await client.connect();
// STRINGS
await client.set('key', 'value', { EX: 3600 }); // SET with TTL
const val = await client.get('key'); // GET
await client.del('key'); // DEL
await client.incr('counter'); // INCR
await client.incrBy('counter', 10); // INCRBY
// HASHES
await client.hSet('user:1', { name: 'Alice', age: '30' });
const name = await client.hGet('user:1', 'name');
const all = await client.hGetAll('user:1');
await client.hIncrBy('user:1', 'views', 1);
// LISTS
await client.lPush('queue', 'item');
await client.rPush('queue', 'item');
const items = await client.lRange('queue', 0, -1);
const popped = await client.lPop('queue');
await client.lTrim('feed', 0, 99); // Keep first 100
// SETS
await client.sAdd('tags', ['js', 'redis']);
const members = await client.sMembers('tags');
const isMember = await client.sIsMember('tags', 'js');
const common = await client.sInter(['set1', 'set2']);
// SORTED SETS
await client.zAdd('board', [{ score: 100, value: 'Alice' }]);
const top = await client.zRange('board', 0, 9, { REV: true });
const rank = await client.zRevRank('board', 'Alice');
await client.zIncrBy('board', 10, 'Alice');
// TTL
await client.expire('key', 3600);
const ttl = await client.ttl('key');
// TRANSACTIONS
const results = await client.multi()
.set('a', '1')
.set('b', '2')
.incr('a')
.exec();
// PUB/SUB (use separate client for subscriber!)
await publisher.publish('channel', JSON.stringify(data));
await subscriber.subscribe('channel', (msg) => { /* ... */ });
// CLEANUP
await client.quit();
Caching Middleware Pattern
function cache(prefix, ttl = 300) {
return async (req, res, next) => {
try {
const key = `${prefix}:${req.originalUrl}`;
const cached = await redis.get(key);
if (cached) return res.json(JSON.parse(cached));
const orig = res.json.bind(res);
res.json = (body) => {
if (res.statusCode < 300)
redis.set(key, JSON.stringify(body), { EX: ttl }).catch(() => {});
return orig(body);
};
next();
} catch { next(); } // Fail open
};
}
Cache Invalidation Patterns
TTL-BASED
SET key value EX 3600 → Auto-expires in 1 hour
(Safety net, always use)
EVENT-BASED
On CREATE → DEL list caches → SCAN + DEL "products:*"
On UPDATE → DEL specific + list caches
On DELETE → DEL specific + list caches
SCAN (production-safe pattern matching)
let cursor = 0;
do {
result = await client.scan(cursor, { MATCH: 'products:*', COUNT: 100 });
cursor = result.cursor;
if (result.keys.length) await client.del(result.keys);
} while (cursor !== 0);
Pub/Sub Pattern
PUBLISHER SUBSCRIBER
publish(channel, msg) ───────> subscribe(channel, callback)
pSubscribe('events:*', callback)
- Fire-and-forget - Must use SEPARATE Redis client
- No persistence - Cannot run other commands
- All subscribers receive - Pattern matching with pSubscribe
Rate Limiting Pattern
FIXED WINDOW
Key: ratelimit:{ip}:{minute}
INCR key → count
If count == 1: EXPIRE key 60
If count > limit: 429 Too Many Requests
SLIDING WINDOW (Sorted Set)
ZREMRANGEBYSCORE key -inf (now - window)
ZADD key now unique_id
ZCARD key → count
EXPIRE key window
If count > limit: 429
Session Store (connect-redis)
const RedisStore = require('connect-redis').default;
app.use(session({
store: new RedisStore({ client: redisClient, prefix: 'sess:', ttl: 86400 }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 86400000 },
}));
TTL Guidelines
| Data Type | TTL | Reasoning |
|---|---|---|
| User session | 24h | Security vs convenience |
| Product catalog | 1-6h | Infrequent changes |
| Search results | 5-15min | Moderate changes |
| Rate limit counters | 1min | Must be fresh |
| Static config | 24h | Rarely changes |
| Real-time data | 5-30s | Frequent changes |
Persistence Comparison
| Feature | RDB (Snapshots) | AOF (Append Log) |
|---|---|---|
| Data loss | Between snapshots | ~1 second (everysec) |
| File size | Compact | Larger |
| Restart speed | Fast | Slower |
| I/O overhead | Low | Higher |
| Production | Use for backups | Use for durability |
| Combined | Recommended: RDB + AOF together |
Common Pitfalls
| Pitfall | Solution |
|---|---|
Using KEYS * in production | Use SCAN with cursor |
| No TTL on cache entries | Always set TTL as safety net |
| Caching errors/404s | Only cache successful responses |
| Same cache key for different users | Include user ID in key for personalized data |
| Redis failure breaks the app | Wrap in try/catch, fail open to database |
| Storing objects without stringify | Always JSON.stringify() / JSON.parse() |
| Using one client for Pub/Sub + commands | Use separate clients |
<< Previous: Interview Questions | Next Section: 3.17 — Error Handling >>