Redis Deep Dive¶
Redis is the swiss army knife of backend infrastructure It's not your primary database - it's the in-memory layer that makes everything faster , from caching to rate limiting to real-time messaging
Redis Data Structures¶
Every data structure is designed for a specific use case
const Redis = require('ioredis')
const redis = new Redis()
// STRING - simple key-value , caching , counters
await redis.set('user:123:name', 'Mahmoud')
await redis.set('visitor:count', 0)
await redis.incr('visitor:count') // atomic increment
await redis.incrby('visitor:count', 5) // atomic increment by
const count = await redis.get('visitor:count')
// HASH - objects with multiple fields
await redis.hset('user:123', {
name: 'Mahmoud',
email: 'mahmoud@example.com',
role: 'admin'
})
const name = await redis.hget('user:123', 'name')
const allFields = await redis.hgetall('user:123')
await redis.hincrby('user:123', 'loginCount', 1)
// LIST - queues , recent items
await redis.lpush('notifications:123', 'msg1', 'msg2')
await redis.rpop('notifications:123') // pop from right
const length = await redis.llen('notifications:123')
const range = await redis.lrange('notifications:123', 0, -1) // all items
// SET - unique members , tags , presence
await redis.sadd('tags:security', 'sql-injection', 'xss', 'csrf')
const isMember = await redis.sismember('tags:security', 'xss')
const members = await redis.smembers('tags:security')
const count = await redis.scard('tags:security')
const intersect = await redis.sinter('tags:security', 'tags:node')
// SORTED SET - leaderboard , scores , timestamps
await redis.zadd('scores', 100, 'user:1', 85, 'user:2', 95, 'user:3')
const top3 = await redis.zrevrange('scores', 0, 2, 'WITHSCORES')
const rank = await redis.zrevrank('scores', 'user:2') // 2nd place
await redis.zincrby('scores', 10, 'user:3') // add 10 points
Pick the right data structure or you'll write application logic that should happen in Redis A sorted set replaces 50 lines of ranking logic with 1 Redis command
Caching Patterns¶
Cache-Aside (Lazy Loading)¶
async function getUser(id) {
const key = `user:${id}`
let data = await redis.get(key)
if (data) return JSON.parse(data)
data = await db.query('SELECT * FROM users WHERE id = $1', [id])
if (data) {
await redis.setex(key, 3600, JSON.stringify(data))
}
return data
}
Cache-aside works well for read-heavy workloads Downside: first request after cache expiry hits the database - that's the thundering herd problem
Write-Through¶
async function updateUser(id, updates) {
const key = `user:${id}`
// Update database first
const user = await db.query(
'UPDATE users SET name = $1 WHERE id = $2 RETURNING *',
[updates.name, id]
)
// Then update cache
await redis.setex(key, 3600, JSON.stringify(user[0]))
return user[0]
}
Write-through keeps cache and database in sync Slower writes because you hit both systems - but reads never return stale data
Cache Invalidation¶
// Strategy 1: TTL expiry
await redis.setex(key, 3600, JSON.stringify(data))
// Cache auto-expires after 1 hour
// Strategy 2: Delete on update
async function deleteUser(id) {
await db.query('DELETE FROM users WHERE id = $1', [id])
await redis.del(`user:${id}`)
}
// Strategy 3: Pattern-based flush
async function invalidateUserCache(userId) {
const pattern = `user:${userId}:*`
const keys = await redis.keys(pattern)
if (keys.length > 0) {
await redis.del(...keys)
}
}
KEYS blocks Redis in production - it scans all keys synchronously Use SCAN instead for production , or track cache keys in a set for targeted deletion
Session Storage¶
const session = require('express-session')
const RedisStore = require('connect-redis')(session)
app.use(session({
store: new RedisStore({
client: redis,
prefix: 'sess:', // session key prefix
ttl: 86400, // 24 hours in seconds
disableTouch: false // refresh TTL on access
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // no JS access
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}))
Redis session storage means your sessions survive server restarts Without it , restarting your Node process logs everyone out - great for maintenance windows
Rate Limiting¶
Sliding window rate limiter - the standard pattern
const WINDOW_SIZE = 60 // 60 seconds
const MAX_REQUESTS = 100 // 100 requests per window
async function rateLimit(ip) {
const key = `ratelimit:${ip}`
const now = Date.now()
const windowStart = now - WINDOW_SIZE * 1000
// Remove old entries outside window
await redis.zremrangebyscore(key, 0, windowStart)
// Count requests in current window
const count = await redis.zcard(key)
if (count >= MAX_REQUESTS) {
return { allowed: false, remaining: 0 }
}
// Add current request
await redis.zadd(key, now, `${now}:${Math.random()}`)
await redis.expire(key, WINDOW_SIZE)
return { allowed: true, remaining: MAX_REQUESTS - count - 1 }
}
// Express middleware
app.use(async (req, res, next) => {
const result = await rateLimit(req.ip)
res.set('X-RateLimit-Remaining', result.remaining)
if (!result.allowed) {
return res.status(429).json({ error: 'too many requests' })
}
next()
})
Sorted sets make sliding window rate limiting trivial - each request is a score entry , old ones expire naturally
Pub/Sub for Real-Time¶
// Publisher
const Redis = require('ioredis')
const pub = new Redis()
app.post('/api/notify', async (req, res) => {
const { userId, message } = req.body
await pub.publish('notifications', JSON.stringify({
userId,
message,
timestamp: Date.now()
}))
res.json({ sent: true })
})
// Subscriber (separate process or connection)
const sub = new Redis()
sub.subscribe('notifications')
sub.on('message', (channel, message) => {
const event = JSON.parse(message)
// Send via WebSocket to connected client
wss.clients.forEach(client => {
if (client.userId === event.userId) {
client.send(JSON.stringify(event))
}
})
})
// Pattern subscribe - match channel patterns
sub.psubscribe('user:*:events')
sub.on('pmessage', (pattern, channel, message) => {
// channel = "user:123:events"
console.log(`Matched pattern ${pattern} on ${channel}`)
})
Redis pub/sub delivers messages once with no persistence If your subscriber is down , messages are lost forever - use Redis Streams or a message queue for reliability
Redis Security¶
Default Redis has no authentication - that's a disaster waiting to happen
# redis.conf - minimum security config
requirepass your-strong-password-here
bind 127.0.0.1 # only localhost
port 6379
rename-command FLUSHALL "" # disable dangerous commands
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command EVAL "" # disable Lua (if not needed)
aclfile /etc/redis/users.acl # ACL-based auth
// Connect with password
const redis = new Redis({
host: 'localhost',
port: 6379,
password: process.env.REDIS_PASSWORD,
tls: process.env.NODE_ENV === 'production' ? {
rejectUnauthorized: true
} : undefined
})
// ACL user management
// In redis-cli:
// ACL SETUSER appuser on >secure_password +@read +@write ~*
// ACL SETUSER admin on >admin_password +@all ~*
Unsecured Redis on a public IP gets owned by crypto miners within hours Bind to localhost or use a VPC , put auth on it , and disable dangerous commands
Prerequisites¶
- db_04_orms.md - understand DB abstractions before adding caching layers