Redis
Redis (Remote Dictionary Server) is an in-memory data structure store used as a database, cache, message broker, and streaming engine. It delivers sub-millisecond response times by keeping all data in RAM, with optional persistence to disk.
Redis is not just a simple key-value cache โ it supports rich data structures like lists, sets, sorted sets, hashes, streams, and HyperLogLogs. These primitives enable sophisticated patterns that would require multiple round-trips or complex application logic with other systems.
This guide covers all major data structures, caching patterns, pub/sub messaging, session management, rate limiting, Lua scripting, and clustering.
note
Redis stores everything as key-value pairs where the value can be one of several data structures. Understanding these structures is essential for using Redis effectively.
| 1 | # Strings: binary-safe, up to 512MB |
| 2 | SET user:1:name "Alice Johnson" |
| 3 | GET user:1:name |
| 4 | MSET user:1:email "alice@example.com" user:1:role "admin" |
| 5 | MGET user:1:email user:1:role |
| 6 | INCR page:home:views # atomic increment |
| 7 | INCRBY product:123:stock 5 # increment by 5 |
| 8 | SETEX session:abc123 3600 "{\"userId\": \"u1\"}" # set with TTL |
| 9 | SETNX lock:order:456 1 # set if not exists (distributed lock) |
| 10 | |
| 11 | # Hashes: field-value maps (like objects) |
| 12 | HSET user:1 name "Alice" email "alice@example.com" age 32 |
| 13 | HGET user:1 name |
| 14 | HGETALL user:1 |
| 15 | HINCRBY user:1 age 1 # atomic field increment |
| 16 | HDEL user:1 age |
| 17 | HEXISTS user:1 name |
| 18 | |
| 19 | # Lists: doubly-linked lists (queues and stacks) |
| 20 | LPUSH notifications:u1 "New order received" |
| 21 | RPUSH notifications:u1 "Payment processed" |
| 22 | LPOP notifications:u1 # left pop (FIFO queue) |
| 23 | RPOP notifications:u1 # right pop (LIFO stack) |
| 24 | LRANGE notifications:u1 0 -1 # get all items |
| 25 | LLEN notifications:u1 # list length |
| 26 | BLPOP notifications:u1 30 # blocking pop (wait 30s) |
| 27 | |
| 28 | # Sets: unordered unique elements |
| 29 | SADD user:1:friends "u2" "u3" "u4" |
| 30 | SMEMBERS user:1:friends |
| 31 | SISMEMBER user:1:friends "u2" # check membership |
| 32 | SINTER user:1:friends user:2:friends # intersection |
| 33 | SUNION user:1:friends user:2:friends # union |
| 34 | SDIFF user:1:friends user:2:friends # difference |
| 35 | SRANDMEMBER user:1:friends 3 # random 3 members |
| 36 | |
| 37 | # Sorted Sets: ordered by score (rankings, leaderboards) |
| 38 | ZADD leaderboard 1500 "player:1" |
| 39 | ZADD leaderboard 2300 "player:2" |
| 40 | ZADD leaderboard 1800 "player:3" |
| 41 | ZRANK leaderboard "player:2" # rank (0-indexed) |
| 42 | ZRANGE leaderboard 0 9 WITHSCORES # top 10 |
| 43 | ZREVRANGE leaderboard 0 4 # top 5 (reverse) |
| 44 | ZINCRBY leaderboard 100 "player:1" # increment score |
info
Caching is the most common use case for Redis. The strategy you choose depends on your data freshness requirements and read/write patterns.
| 1 | import { createClient } from "redis"; |
| 2 | |
| 3 | const redis = createClient({ url: process.env.REDIS_URL }); |
| 4 | await redis.connect(); |
| 5 | |
| 6 | // Pattern 1: Cache-Aside (Lazy Loading) |
| 7 | // Application manages cache explicitly |
| 8 | async function getUserProfile(userId: string) { |
| 9 | const cacheKey = `user:${userId}:profile`; |
| 10 | |
| 11 | // Try cache first |
| 12 | const cached = await redis.get(cacheKey); |
| 13 | if (cached) { |
| 14 | return JSON.parse(cached); |
| 15 | } |
| 16 | |
| 17 | // Cache miss โ fetch from database |
| 18 | const user = await db.user.findUnique({ where: { id: userId } }); |
| 19 | if (!user) return null; |
| 20 | |
| 21 | // Populate cache with 5-minute TTL |
| 22 | await redis.setEx(cacheKey, 300, JSON.stringify(user)); |
| 23 | return user; |
| 24 | } |
| 25 | |
| 26 | // Pattern 2: Write-Through |
| 27 | // Write to cache and database simultaneously |
| 28 | async function updateUserProfile(userId: string, data: ProfileData) { |
| 29 | const user = await db.user.update({ where: { id: userId }, data }); |
| 30 | await redis.setEx(`user:${userId}:profile`, 300, JSON.stringify(user)); |
| 31 | return user; |
| 32 | } |
| 33 | |
| 34 | // Pattern 3: Write-Behind (Write-Back) |
| 35 | // Write to cache first, asynchronously flush to database |
| 36 | async function incrementPageViews(pageId: string) { |
| 37 | const key = `page:${pageId}:views`; |
| 38 | await redis.incr(key); |
| 39 | |
| 40 | // Schedule async write to database |
| 41 | // (using a message queue or background job) |
| 42 | await queue.add("flush-views", { pageId, key }); |
| 43 | } |
| 44 | |
| 45 | // Pattern 4: Cache Invalidation |
| 46 | async function invalidateUserCache(userId: string) { |
| 47 | // Delete specific keys |
| 48 | await redis.del(`user:${userId}:profile`); |
| 49 | |
| 50 | // Delete pattern matches |
| 51 | const keys = await redis.keys(`user:${userId}:*`); |
| 52 | if (keys.length > 0) { |
| 53 | await redis.del(keys); |
| 54 | } |
| 55 | } |
pro tip
Redis provides two messaging mechanisms: Pub/Sub for real-time broadcast messaging, and Streams for durable, consumer-group-based message processing (similar to Kafka).
| 1 | // Pub/Sub: Simple real-time messaging |
| 2 | // Publisher |
| 3 | await redis.publish("notifications", JSON.stringify({ |
| 4 | type: "order_shipped", |
| 5 | userId: "u1", |
| 6 | orderId: "o123", |
| 7 | timestamp: new Date().toISOString() |
| 8 | })); |
| 9 | |
| 10 | // Subscriber |
| 11 | const subscriber = redis.duplicate(); |
| 12 | await subscriber.connect(); |
| 13 | await subscriber.subscribe("notifications", (message) => { |
| 14 | const event = JSON.parse(message); |
| 15 | console.log("Received:", event); |
| 16 | // Send push notification, update UI, etc. |
| 17 | }); |
| 18 | |
| 19 | // Pattern-based subscriptions |
| 20 | await subscriber.pSubscribe("orders:*", (message, channel) => { |
| 21 | console.log(`Received on ${channel}:`, message); |
| 22 | }); |
| 23 | |
| 24 | // Streams: Durable message processing (Kafka-like) |
| 25 | // Producer: add messages to a stream |
| 26 | await redis.xAdd("orders:stream", "*", { |
| 27 | type: "new_order", |
| 28 | orderId: "o123", |
| 29 | userId: "u1", |
| 30 | total: "99.99" |
| 31 | }); |
| 32 | |
| 33 | // Consumer Group: multiple consumers process messages |
| 34 | await redis.xGroupCreate("orders:stream", "processing-group", "0"); |
| 35 | |
| 36 | // Consumer reads messages |
| 37 | const messages = await redis.xReadGroup( |
| 38 | "processing-group", |
| 39 | "consumer-1", |
| 40 | [{ key: "orders:stream", id: ">" }], |
| 41 | { COUNT: 10, BLOCK: 5000 } |
| 42 | ); |
| 43 | |
| 44 | // Acknowledge processed messages |
| 45 | for (const stream of messages) { |
| 46 | for (const message of stream.messages) { |
| 47 | // Process message... |
| 48 | await redis.xAck("orders:stream", "processing-group", message.id); |
| 49 | } |
| 50 | } |
| 51 | |
| 52 | // Stream consumer with retry (pending messages) |
| 53 | const pending = await redis.xPending("orders:stream", "processing-group", { |
| 54 | START: "0", |
| 55 | END: "+", |
| 56 | COUNT: 10 |
| 57 | }); |
warning
Redis is the standard choice for session storage in web applications. Its sub-millisecond latency, TTL support, and atomic operations make it ideal for managing user sessions at scale.
| 1 | import { createClient } from "redis"; |
| 2 | import crypto from "crypto"; |
| 3 | |
| 4 | const redis = createClient({ url: process.env.REDIS_URL }); |
| 5 | await redis.connect(); |
| 6 | |
| 7 | const SESSION_TTL = 60 * 60 * 24; // 24 hours |
| 8 | |
| 9 | interface Session { |
| 10 | userId: string; |
| 11 | role: string; |
| 12 | email: string; |
| 13 | createdAt: number; |
| 14 | lastActivity: number; |
| 15 | } |
| 16 | |
| 17 | // Create session |
| 18 | async function createSession(userId: string, userData: Session): Promise<string> { |
| 19 | const sessionId = crypto.randomUUID(); |
| 20 | const key = `session:${sessionId}`; |
| 21 | |
| 22 | await redis.setEx(key, SESSION_TTL, JSON.stringify({ |
| 23 | ...userData, |
| 24 | createdAt: Date.now(), |
| 25 | lastActivity: Date.now() |
| 26 | })); |
| 27 | |
| 28 | // Track active sessions for a user |
| 29 | await redis.sAdd(`user:${userId}:sessions`, sessionId); |
| 30 | await redis.expire(`user:${userId}:sessions`, SESSION_TTL); |
| 31 | |
| 32 | return sessionId; |
| 33 | } |
| 34 | |
| 35 | // Get session (with automatic TTL refresh) |
| 36 | async function getSession(sessionId: string): Promise<Session | null> { |
| 37 | const key = `session:${sessionId}`; |
| 38 | const data = await redis.get(key); |
| 39 | if (!data) return null; |
| 40 | |
| 41 | const session = JSON.parse(data); |
| 42 | |
| 43 | // Extend TTL on activity (sliding window) |
| 44 | await redis.expire(key, SESSION_TTL); |
| 45 | session.lastActivity = Date.now(); |
| 46 | |
| 47 | return session; |
| 48 | } |
| 49 | |
| 50 | // Destroy session (logout) |
| 51 | async function destroySession(sessionId: string): Promise<void> { |
| 52 | const key = `session:${sessionId}`; |
| 53 | const data = await redis.get(key); |
| 54 | if (!data) return; |
| 55 | |
| 56 | const session = JSON.parse(data); |
| 57 | await redis.del(key); |
| 58 | await redis.sRem(`user:${session.userId}:sessions`, sessionId); |
| 59 | } |
| 60 | |
| 61 | // Invalidate all sessions for a user (security breach) |
| 62 | async function destroyAllUserSessions(userId: string): Promise<void> { |
| 63 | const sessionIds = await redis.sMembers(`user:${userId}:sessions`); |
| 64 | if (sessionIds.length > 0) { |
| 65 | const keys = sessionIds.map(id => `session:${id}`); |
| 66 | await redis.del(keys); |
| 67 | } |
| 68 | await redis.del(`user:${userId}:sessions`); |
| 69 | } |
Redis enables several rate limiting algorithms, from simple token buckets to sliding window counters. These protect your API from abuse while maintaining low latency.
| 1 | // Algorithm 1: Fixed Window Counter |
| 2 | async function fixedWindowRateLimit( |
| 3 | key: string, |
| 4 | limit: number, |
| 5 | windowSeconds: number |
| 6 | ): Promise<{ allowed: boolean; remaining: number }> { |
| 7 | const window = Math.floor(Date.now() / (windowSeconds * 1000)); |
| 8 | const redisKey = `ratelimit:${key}:${window}`; |
| 9 | |
| 10 | const count = await redis.incr(redisKey); |
| 11 | if (count === 1) { |
| 12 | await redis.expire(redisKey, windowSeconds); |
| 13 | } |
| 14 | |
| 15 | return { |
| 16 | allowed: count <= limit, |
| 17 | remaining: Math.max(0, limit - count) |
| 18 | }; |
| 19 | } |
| 20 | |
| 21 | // Algorithm 2: Sliding Window Log (more accurate) |
| 22 | async function slidingWindowRateLimit( |
| 23 | key: string, |
| 24 | limit: number, |
| 25 | windowSeconds: number |
| 26 | ): Promise<{ allowed: boolean; remaining: number }> { |
| 27 | const now = Date.now(); |
| 28 | const windowStart = now - (windowSeconds * 1000); |
| 29 | const redisKey = `ratelimit:${key}`; |
| 30 | |
| 31 | // Remove old entries |
| 32 | await redis.zRemRangeByScore(redisKey, 0, windowStart); |
| 33 | |
| 34 | // Count current window |
| 35 | const count = await redis.zCard(redisKey); |
| 36 | |
| 37 | if (count < limit) { |
| 38 | // Add current request |
| 39 | await redis.zAdd(redisKey, { score: now, value: `${now}:${crypto.randomUUID()}` }); |
| 40 | await redis.expire(redisKey, windowSeconds); |
| 41 | return { allowed: true, remaining: limit - count - 1 }; |
| 42 | } |
| 43 | |
| 44 | return { allowed: false, remaining: 0 }; |
| 45 | } |
| 46 | |
| 47 | // Algorithm 3: Token Bucket (smooths bursts) |
| 48 | async function tokenBucketRateLimit( |
| 49 | key: string, |
| 50 | capacity: number, |
| 51 | refillRate: number, |
| 52 | refillIntervalMs: number |
| 53 | ): Promise<{ allowed: boolean; remaining: number; retryAfterMs: number }> { |
| 54 | const luaScript = ` |
| 55 | local key = KEYS[1] |
| 56 | local capacity = tonumber(ARGV[1]) |
| 57 | local refill_rate = tonumber(ARGV[2]) |
| 58 | local refill_interval = tonumber(ARGV[3]) |
| 59 | local now = tonumber(ARGV[4]) |
| 60 | |
| 61 | local bucket = redis.call('HMGET', key, 'tokens', 'last_refill') |
| 62 | local tokens = tonumber(bucket[1]) or capacity |
| 63 | local last_refill = tonumber(bucket[2]) or now |
| 64 | |
| 65 | -- Refill tokens |
| 66 | local elapsed = now - last_refill |
| 67 | local refill_count = math.floor(elapsed / refill_interval) * refill_rate |
| 68 | tokens = math.min(capacity, tokens + refill_count) |
| 69 | local new_last_refill = last_refill + math.floor(elapsed / refill_interval) * refill_interval |
| 70 | |
| 71 | if tokens >= 1 then |
| 72 | tokens = tokens - 1 |
| 73 | redis.call('HMSET', key, 'tokens', tokens, 'last_refill', new_last_refill) |
| 74 | redis.call('EXPIRE', key, math.ceil(capacity / refill_rate) * refill_interval / 1000) |
| 75 | return {1, tokens, 0} |
| 76 | else |
| 77 | local wait = refill_interval - (elapsed % refill_interval) |
| 78 | return {0, tokens, wait} |
| 79 | end |
| 80 | `; |
| 81 | |
| 82 | const result = await redis.eval(luaScript, { |
| 83 | keys: [`bucket:${key}`], |
| 84 | arguments: [capacity, refillRate, refillIntervalMs, Date.now()] |
| 85 | }); |
| 86 | |
| 87 | return { |
| 88 | allowed: result[0] === 1, |
| 89 | remaining: result[1], |
| 90 | retryAfterMs: result[2] |
| 91 | }; |
| 92 | } |
info
Redis provides a simple mechanism for distributed locks using the SET NX EX pattern. This is essential for coordinating access to shared resources across multiple application instances.
| 1 | // Simple distributed lock |
| 2 | async function acquireLock( |
| 3 | key: string, |
| 4 | ttlMs: number |
| 5 | ): Promise<string | null> { |
| 6 | const token = crypto.randomUUID(); |
| 7 | const acquired = await redis.set(`lock:${key}`, token, { |
| 8 | NX: true, // Only set if not exists |
| 9 | PX: ttlMs // Expire in milliseconds |
| 10 | }); |
| 11 | return acquired ? token : null; |
| 12 | } |
| 13 | |
| 14 | async function releaseLock(key: string, token: string): Promise<boolean> { |
| 15 | // Lua script for atomic check-and-delete |
| 16 | const script = ` |
| 17 | if redis.call("get", KEYS[1]) == ARGV[1] then |
| 18 | return redis.call("del", KEYS[1]) |
| 19 | else |
| 20 | return 0 |
| 21 | end |
| 22 | `; |
| 23 | const result = await redis.eval(script, { |
| 24 | keys: [`lock:${key}`], |
| 25 | arguments: [token] |
| 26 | }); |
| 27 | return result === 1; |
| 28 | } |
| 29 | |
| 30 | // Usage: process order atomically |
| 31 | async function processOrder(orderId: string) { |
| 32 | const lockToken = await acquireLock(`order:${orderId}`, 10000); |
| 33 | |
| 34 | if (!lockToken) { |
| 35 | throw new Error("Could not acquire lock โ another instance is processing this order"); |
| 36 | } |
| 37 | |
| 38 | try { |
| 39 | // Process the order... |
| 40 | await processPayment(orderId); |
| 41 | await updateInventory(orderId); |
| 42 | await sendConfirmation(orderId); |
| 43 | } finally { |
| 44 | await releaseLock(`order:${orderId}`, lockToken); |
| 45 | } |
| 46 | } |
danger
Lua scripts execute atomically on the Redis server, enabling complex multi-step operations without race conditions. They are the foundation of advanced patterns like token buckets and optimistic locking.
| 1 | // Atomic check-and-set with Lua |
| 2 | const checkAndSetScript = ` |
| 3 | local current = redis.call("GET", KEYS[1]) |
| 4 | if current == ARGV[1] then |
| 5 | redis.call("SET", KEYS[1], ARGV[2]) |
| 6 | return 1 |
| 7 | end |
| 8 | return 0 |
| 9 | `; |
| 10 | |
| 11 | await redis.eval(checkAndSetScript, { |
| 12 | keys: ["counter"], |
| 13 | arguments: ["old_value", "new_value"] |
| 14 | }); |
| 15 | |
| 16 | // Atomic increment with max cap |
| 17 | const cappedIncrementScript = ` |
| 18 | local current = tonumber(redis.call("GET", KEYS[1]) or "0") |
| 19 | local max = tonumber(ARGV[1]) |
| 20 | local increment = tonumber(ARGV[2]) |
| 21 | |
| 22 | if current + increment > max then |
| 23 | return -1 -- would exceed cap |
| 24 | end |
| 25 | |
| 26 | redis.call("INCRBY", KEYS[1], increment) |
| 27 | return current + increment |
| 28 | `; |
| 29 | |
| 30 | const result = await redis.eval(cappedIncrementScript, { |
| 31 | keys: ["rate:user123"], |
| 32 | arguments: ["100", "1"] |
| 33 | }); |
| 34 | |
| 35 | // Cache stampede prevention (singleflight) |
| 36 | const singleflightScript = ` |
| 37 | local key = KEYS[1] |
| 38 | local ttl = tonumber(ARGV[1]) |
| 39 | local lockKey = key .. ":lock" |
| 40 | |
| 41 | local value = redis.call("GET", key) |
| 42 | if value then return value end |
| 43 | |
| 44 | local locked = redis.call("SET", lockKey, "1", "NX", "PX", ttl) |
| 45 | if locked then |
| 46 | return "LOCKED" |
| 47 | else |
| 48 | return "BUSY" |
| 49 | end |
| 50 | `; |
Redis Cluster provides horizontal scaling by sharding data across multiple nodes. Redis also supports persistence options for data durability.
| 1 | # Redis Cluster: automatic sharding across nodes |
| 2 | # Node 1: slots 0-5460 |
| 3 | # Node 2: slots 5461-10922 |
| 4 | # Node 3: slots 10923-16383 |
| 5 | |
| 6 | # Create a cluster |
| 7 | redis-cli --cluster create \ |
| 8 | 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 \ |
| 9 | --cluster-replicas 1 |
| 10 | |
| 11 | # Check cluster status |
| 12 | redis-cli -p 7001 cluster info |
| 13 | redis-cli -p 7001 cluster nodes |
| 14 | |
| 15 | # Hash tags: force keys to same slot |
| 16 | SET {user:1}:name "Alice" # both go to same slot |
| 17 | SET {user:1}:email "a@b.com" # because of {user:1} |
| 18 | |
| 19 | # Persistence options: |
| 20 | # RDB (snapshots) โ fast, point-in-time recovery |
| 21 | save 900 1 # save if 1 key changed in 900 seconds |
| 22 | save 300 10 # save if 10 keys changed in 300 seconds |
| 23 | save 60 10000 # save if 10000 keys changed in 60 seconds |
| 24 | |
| 25 | # AOF (append-only file) โ more durable, slower recovery |
| 26 | appendonly yes |
| 27 | appendfsync everysec # fsync every second (recommended) |
| 28 | appendfsync always # fsync every write (slowest, most durable) |
| 29 | |
| 30 | # Replication for high availability |
| 31 | # redis.conf on replica: |
| 32 | replicaof 127.0.0.1 6379 |
| 33 | masterauth <password> |
| 34 | |
| 35 | # Redis Sentinel: automatic failover |
| 36 | sentinel monitor mymaster 127.0.0.1 6379 2 |
| 37 | sentinel down-after-milliseconds mymaster 5000 |
| 38 | sentinel failover-timeout mymaster 60000 |
info
The most popular Redis client for Node.js is theredis package (node-redis). It supports all Redis commands, Lua scripting, pub/sub, and cluster mode.
| 1 | import { createClient, createCluster } from "redis"; |
| 2 | |
| 3 | // Basic client setup |
| 4 | const client = createClient({ |
| 5 | url: process.env.REDIS_URL, |
| 6 | socket: { |
| 7 | reconnectStrategy: (retries) => { |
| 8 | if (retries > 10) return new Error("Max retries reached"); |
| 9 | return Math.min(retries * 100, 3000); // exponential backoff |
| 10 | } |
| 11 | } |
| 12 | }); |
| 13 | |
| 14 | client.on("error", (err) => console.error("Redis Client Error", err)); |
| 15 | client.on("connect", () => console.log("Redis connected")); |
| 16 | client.on("reconnecting", () => console.log("Redis reconnecting")); |
| 17 | |
| 18 | await client.connect(); |
| 19 | |
| 20 | // Pipeline: batch multiple commands (single round-trip) |
| 21 | const pipeline = client.multi(); |
| 22 | pipeline.set("key1", "value1"); |
| 23 | pipeline.set("key2", "value2"); |
| 24 | pipeline.get("key1"); |
| 25 | pipeline.incr("counter"); |
| 26 | const results = await pipeline.exec(); |
| 27 | |
| 28 | // Cluster client |
| 29 | const cluster = createCluster({ |
| 30 | rootNodes: [ |
| 31 | { url: "redis://127.0.0.1:7001" }, |
| 32 | { url: "redis://127.0.0.1:7002" }, |
| 33 | { url: "redis://127.0.0.1:7003" } |
| 34 | ] |
| 35 | }); |
| 36 | await cluster.connect(); |
| 37 | |
| 38 | // Use cluster the same way as a regular client |
| 39 | await cluster.set("key", "value"); |
| 40 | const val = await cluster.get("key"); |