|$ curl https://forge-ai.dev/api/markdown?path=docs/databases/redis
$cat docs/redis.md
updated Recentlyยท50 min readยทpublished

Redis

โ—†Redisโ—†Cachingโ—†Key-Valueโ—†Beginner to Advanced๐ŸŽฏFree Tools
Introduction

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 7.x introduced Redis Functions (replacing EVAL for Lua scripts), improved sharding, and client-side caching. Redis Stack adds modules for JSON, time-series, search, and probabilistic data structures.
Data Structures

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.

data-structures.sh
Bash
1# Strings: binary-safe, up to 512MB
2SET user:1:name "Alice Johnson"
3GET user:1:name
4MSET user:1:email "alice@example.com" user:1:role "admin"
5MGET user:1:email user:1:role
6INCR page:home:views # atomic increment
7INCRBY product:123:stock 5 # increment by 5
8SETEX session:abc123 3600 "{\"userId\": \"u1\"}" # set with TTL
9SETNX lock:order:456 1 # set if not exists (distributed lock)
10
11# Hashes: field-value maps (like objects)
12HSET user:1 name "Alice" email "alice@example.com" age 32
13HGET user:1 name
14HGETALL user:1
15HINCRBY user:1 age 1 # atomic field increment
16HDEL user:1 age
17HEXISTS user:1 name
18
19# Lists: doubly-linked lists (queues and stacks)
20LPUSH notifications:u1 "New order received"
21RPUSH notifications:u1 "Payment processed"
22LPOP notifications:u1 # left pop (FIFO queue)
23RPOP notifications:u1 # right pop (LIFO stack)
24LRANGE notifications:u1 0 -1 # get all items
25LLEN notifications:u1 # list length
26BLPOP notifications:u1 30 # blocking pop (wait 30s)
27
28# Sets: unordered unique elements
29SADD user:1:friends "u2" "u3" "u4"
30SMEMBERS user:1:friends
31SISMEMBER user:1:friends "u2" # check membership
32SINTER user:1:friends user:2:friends # intersection
33SUNION user:1:friends user:2:friends # union
34SDIFF user:1:friends user:2:friends # difference
35SRANDMEMBER user:1:friends 3 # random 3 members
36
37# Sorted Sets: ordered by score (rankings, leaderboards)
38ZADD leaderboard 1500 "player:1"
39ZADD leaderboard 2300 "player:2"
40ZADD leaderboard 1800 "player:3"
41ZRANK leaderboard "player:2" # rank (0-indexed)
42ZRANGE leaderboard 0 9 WITHSCORES # top 10
43ZREVRANGE leaderboard 0 4 # top 5 (reverse)
44ZINCRBY leaderboard 100 "player:1" # increment score
โ„น

info

Every Redis command is atomic. You never need transactions for simple operations like incrementing a counter. Use INCR/INCRBY for atomic counters, and Lua scripts for multi-step atomic operations.
Caching Strategies

Caching is the most common use case for Redis. The strategy you choose depends on your data freshness requirements and read/write patterns.

caching-patterns.ts
TypeScript
1import { createClient } from "redis";
2
3const redis = createClient({ url: process.env.REDIS_URL });
4await redis.connect();
5
6// Pattern 1: Cache-Aside (Lazy Loading)
7// Application manages cache explicitly
8async 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
28async 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
36async 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
46async 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

Cache-Aside is the most common pattern. It is simple, predictable, and handles cache misses gracefully. Write-Behind is powerful for write-heavy workloads but adds complexity around data loss during Redis failures.
Pub/Sub and Streams

Redis provides two messaging mechanisms: Pub/Sub for real-time broadcast messaging, and Streams for durable, consumer-group-based message processing (similar to Kafka).

pubsub-streams.ts
TypeScript
1// Pub/Sub: Simple real-time messaging
2// Publisher
3await redis.publish("notifications", JSON.stringify({
4 type: "order_shipped",
5 userId: "u1",
6 orderId: "o123",
7 timestamp: new Date().toISOString()
8}));
9
10// Subscriber
11const subscriber = redis.duplicate();
12await subscriber.connect();
13await 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
20await 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
26await 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
34await redis.xGroupCreate("orders:stream", "processing-group", "0");
35
36// Consumer reads messages
37const 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
45for (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)
53const pending = await redis.xPending("orders:stream", "processing-group", {
54 START: "0",
55 END: "+",
56 COUNT: 10
57});
โš 

warning

Pub/Sub messages are fire-and-forget โ€” if a subscriber is offline when a message is published, it is lost. Use Streams for durable messaging where message loss is unacceptable.
Session Management

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.

sessions.ts
TypeScript
1import { createClient } from "redis";
2import crypto from "crypto";
3
4const redis = createClient({ url: process.env.REDIS_URL });
5await redis.connect();
6
7const SESSION_TTL = 60 * 60 * 24; // 24 hours
8
9interface Session {
10 userId: string;
11 role: string;
12 email: string;
13 createdAt: number;
14 lastActivity: number;
15}
16
17// Create session
18async 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)
36async 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)
51async 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)
62async 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}
Rate Limiting

Redis enables several rate limiting algorithms, from simple token buckets to sliding window counters. These protect your API from abuse while maintaining low latency.

rate-limiting.ts
TypeScript
1// Algorithm 1: Fixed Window Counter
2async 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)
22async 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)
48async 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

The sliding window log is the most accurate but uses more memory (stores every request timestamp). The fixed window counter is memory-efficient but can allow up to 2x the limit at window boundaries. Token bucket smooths bursts and is ideal for API rate limiting.
Distributed Locks

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.

distributed-locks.ts
TypeScript
1// Simple distributed lock
2async 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
14async 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
31async 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

For production distributed locks, consider using Redlock (Redis's distributed lock algorithm) or a dedicated solution like etcd/Consul. Simple SET NX EX can fail during network partitions or Redis failovers.
Lua Scripting

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.

lua-scripting.ts
TypeScript
1// Atomic check-and-set with Lua
2const 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
11await redis.eval(checkAndSetScript, {
12 keys: ["counter"],
13 arguments: ["old_value", "new_value"]
14});
15
16// Atomic increment with max cap
17const 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
30const result = await redis.eval(cappedIncrementScript, {
31 keys: ["rate:user123"],
32 arguments: ["100", "1"]
33});
34
35// Cache stampede prevention (singleflight)
36const 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 and Persistence

Redis Cluster provides horizontal scaling by sharding data across multiple nodes. Redis also supports persistence options for data durability.

cluster-persistence.sh
Bash
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
7redis-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
12redis-cli -p 7001 cluster info
13redis-cli -p 7001 cluster nodes
14
15# Hash tags: force keys to same slot
16SET {user:1}:name "Alice" # both go to same slot
17SET {user:1}:email "a@b.com" # because of {user:1}
18
19# Persistence options:
20# RDB (snapshots) โ€” fast, point-in-time recovery
21save 900 1 # save if 1 key changed in 900 seconds
22save 300 10 # save if 10 keys changed in 300 seconds
23save 60 10000 # save if 10000 keys changed in 60 seconds
24
25# AOF (append-only file) โ€” more durable, slower recovery
26appendonly yes
27appendfsync everysec # fsync every second (recommended)
28appendfsync always # fsync every write (slowest, most durable)
29
30# Replication for high availability
31# redis.conf on replica:
32replicaof 127.0.0.1 6379
33masterauth <password>
34
35# Redis Sentinel: automatic failover
36sentinel monitor mymaster 127.0.0.1 6379 2
37sentinel down-after-milliseconds mymaster 5000
38sentinel failover-timeout mymaster 60000
โ„น

info

For production deployments, use Redis Cluster with at least 3 masters and 3 replicas. Enable both RDB and AOF persistence for data durability. Use Redis Sentinel for automatic failover in non-clustered setups.
Node.js Integration

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.

nodejs-redis.ts
TypeScript
1import { createClient, createCluster } from "redis";
2
3// Basic client setup
4const 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
14client.on("error", (err) => console.error("Redis Client Error", err));
15client.on("connect", () => console.log("Redis connected"));
16client.on("reconnecting", () => console.log("Redis reconnecting"));
17
18await client.connect();
19
20// Pipeline: batch multiple commands (single round-trip)
21const pipeline = client.multi();
22pipeline.set("key1", "value1");
23pipeline.set("key2", "value2");
24pipeline.get("key1");
25pipeline.incr("counter");
26const results = await pipeline.exec();
27
28// Cluster client
29const 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});
36await cluster.connect();
37
38// Use cluster the same way as a regular client
39await cluster.set("key", "value");
40const val = await cluster.get("key");