Ideas Engineered for Tomorrow
We Engineer Services & Solutions for Your Business Needs
Home About
Products
Services
Hire
Industries
Consulting
Partners
Articles Careers Contact
Software Development

Redis Caching Patterns: Beyond Simple Key-Value

Most teams use Redis as a dumb cache — SET key, GET key, done. But Redis is a full-featured data structure server. Here are the patterns that unlock its real power.

⚡ Database & Data February 4, 2026 11 min read

In This Guide

Redis handles 100,000+ operations per second on a single thread. It's not just fast — its data structures (sorted sets, streams, hashes, HyperLogLog) solve problems that would otherwise require complex application code. This guide covers the patterns we use most in production systems.

1. Redis Data Structures — Your Toolkit

Structure Commands Use Case Complexity
StringGET, SET, INCR, SETEXCache, counters, simple valuesO(1)
HashHGET, HSET, HGETALLUser profiles, object storageO(1) per field
ListLPUSH, RPOP, LRANGEQueues, recent activity feedsO(1) push/pop
SetSADD, SISMEMBER, SINTERTags, unique visitors, mutual friendsO(1) per op
Sorted SetZADD, ZRANGEBYSCORE, ZRANKLeaderboards, priority queues, time seriesO(log N)
StreamXADD, XREAD, XREADGROUPEvent sourcing, message queuesO(1) per entry
HyperLogLogPFADD, PFCOUNTUnique counts (12KB for billions)O(1)

2. Caching Patterns That Actually Work

Cache-Aside with Stampede Protection

// Problem: 1000 requests hit cache miss simultaneously → 1000 DB queries
// Solution: Lock + early expiration

async function getProduct(productId) {
    const key = `product:${productId}`;
    const lockKey = `lock:${key}`;

    // Check cache
    const cached = await redis.get(key);
    if (cached) {
        const data = JSON.parse(cached);
        // Early refresh: if within 10% of TTL, refresh in background
        const ttl = await redis.ttl(key);
        if (ttl < 30) {  // Less than 30s remaining on a 300s TTL
            refreshInBackground(key, productId);  // Non-blocking
        }
        return data;
    }

    // Cache miss — try to acquire lock (only one request queries DB)
    const acquired = await redis.set(lockKey, '1', 'NX', 'EX', 5);

    if (acquired) {
        // We got the lock — query DB and populate cache
        const product = await db.query('SELECT * FROM products WHERE id = $1', [productId]);
        await redis.setex(key, 300, JSON.stringify(product));
        await redis.del(lockKey);
        return product;
    }

    // Another request is fetching — wait briefly and retry
    await sleep(50);
    return getProduct(productId);  // Retry (will likely hit cache now)
}

Cache Invalidation Strategies

# Strategy 1: Delete on write (simplest, most common)
async function updateProduct(id, data) {
    await db.update('products', id, data);
    await redis.del(`product:${id}`);           // Delete, don't update
    await redis.del(`product_list:category:${data.category}`);  // Related caches too
}

# Strategy 2: Tag-based invalidation (for complex dependencies)
async function cacheWithTags(key, value, tags, ttl) {
    await redis.setex(key, ttl, JSON.stringify(value));
    for (const tag of tags) {
        await redis.sadd(`tag:${tag}`, key);    // Track which keys use this tag
    }
}

async function invalidateTag(tag) {
    const keys = await redis.smembers(`tag:${tag}`);
    if (keys.length) await redis.del(...keys);  // Delete all keys with this tag
    await redis.del(`tag:${tag}`);
}

// Usage:
await cacheWithTags('product:123', product, ['products', 'category:electronics'], 300);
await invalidateTag('category:electronics');    // Invalidates ALL electronics products

# Strategy 3: Versioned keys (never invalidate — just bump version)
const version = await redis.incr('product_version:123');
const key = `product:123:v${version}`;
await redis.setex(key, 300, JSON.stringify(product));
// Old versioned keys expire naturally via TTL

3. Rate Limiting

Sliding Window Rate Limiter

// Allow 100 requests per minute per user (sliding window)
async function rateLimit(userId, limit = 100, windowSecs = 60) {
    const key = `ratelimit:${userId}`;
    const now = Date.now();
    const windowStart = now - (windowSecs * 1000);

    // Atomic operation using a sorted set
    const pipe = redis.pipeline();
    pipe.zremrangebyscore(key, 0, windowStart);    // Remove old entries
    pipe.zadd(key, now, `${now}:${Math.random()}`); // Add current request
    pipe.zcard(key);                                  // Count requests in window
    pipe.expire(key, windowSecs);                     // Auto-cleanup
    const results = await pipe.exec();

    const requestCount = results[2][1];

    if (requestCount > limit) {
        return { allowed: false, remaining: 0, retryAfter: windowSecs };
    }

    return { allowed: true, remaining: limit - requestCount };
}

// Usage in Express middleware
app.use(async (req, res, next) => {
    const result = await rateLimit(req.ip);
    res.set('X-RateLimit-Remaining', result.remaining);
    if (!result.allowed) {
        res.set('Retry-After', result.retryAfter);
        return res.status(429).json({ error: 'Rate limit exceeded' });
    }
    next();
});

4. Distributed Locks

Redlock Pattern — Safe Distributed Locking

// Simple lock (single Redis instance)
async function acquireLock(resource, ttlMs = 5000) {
    const lockId = crypto.randomUUID();  // Unique per lock holder
    const key = `lock:${resource}`;

    // SET NX = only set if not exists, EX = auto-expire
    const acquired = await redis.set(key, lockId, 'PX', ttlMs, 'NX');

    return acquired ? lockId : null;
}

async function releaseLock(resource, lockId) {
    // Lua script — atomic check-and-delete (prevents releasing someone else's lock)
    const script = `
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `;
    await redis.eval(script, 1, `lock:${resource}`, lockId);
}

// Usage: prevent double-processing an order
async function processOrder(orderId) {
    const lockId = await acquireLock(`order:${orderId}`, 30000);
    if (!lockId) {
        console.log('Order already being processed');
        return;
    }

    try {
        await chargePayment(orderId);
        await updateInventory(orderId);
        await sendConfirmation(orderId);
    } finally {
        await releaseLock(`order:${orderId}`, lockId);
    }
}

5. Leaderboards and Ranking

Real-Time Leaderboard with Sorted Sets

# Add/update scores
ZADD leaderboard 1500 "player:alice"
ZADD leaderboard 2300 "player:bob"
ZADD leaderboard 1800 "player:charlie"
ZINCRBY leaderboard 100 "player:alice"    # Alice now at 1600

# Top 10 players (highest first)
ZREVRANGE leaderboard 0 9 WITHSCORES
# → bob:2300, charlie:1800, alice:1600

# Get player's rank (0-indexed, highest first)
ZREVRANK leaderboard "player:alice"       # → 2 (3rd place)

# Get players ranked 50-60
ZREVRANGE leaderboard 49 59 WITHSCORES

# Weekly leaderboard (auto-expire)
ZADD leaderboard:week:2026-06 1500 "player:alice"
EXPIRE leaderboard:week:2026-06 604800    # 7 days

# Cross-leaderboard: combine daily scores for weekly
ZUNIONSTORE leaderboard:week:2026-06 7 \
    leaderboard:day:2026-02-01 leaderboard:day:2026-02-02 ... \
    AGGREGATE SUM

6. Session Management

// Hash-based sessions — update individual fields without full serialization
async function createSession(userId) {
    const sessionId = crypto.randomUUID();
    const key = `session:${sessionId}`;

    await redis.hmset(key, {
        userId: userId,
        createdAt: Date.now(),
        lastActivity: Date.now(),
        ip: req.ip
    });
    await redis.expire(key, 86400);  // 24-hour TTL

    // Track user's active sessions (for "log out all devices")
    await redis.sadd(`user_sessions:${userId}`, sessionId);

    return sessionId;
}

// Update activity without full read-write cycle
async function touchSession(sessionId) {
    await redis.hset(`session:${sessionId}`, 'lastActivity', Date.now());
    await redis.expire(`session:${sessionId}`, 86400);  // Reset TTL
}

// Log out from all devices
async function logoutAllDevices(userId) {
    const sessions = await redis.smembers(`user_sessions:${userId}`);
    if (sessions.length) {
        await redis.del(...sessions.map(s => `session:${s}`));
    }
    await redis.del(`user_sessions:${userId}`);
}

7. Pub/Sub and Streams

Feature Pub/Sub Streams
PersistenceNo (fire-and-forget)Yes (stored on disk)
Consumer groupsNoYes (Kafka-like)
ReplayNoYes (by ID or time)
Best forReal-time notifications, chatEvent sourcing, task queues

Redis Streams — Lightweight Event Queue

# Producer: add events to stream
XADD orders * customer_id "cust-123" amount "99.99" status "placed"
XADD orders * customer_id "cust-456" amount "149.99" status "placed"

# Create consumer group
XGROUP CREATE orders order-processors $ MKSTREAM

# Consumer: read from group (each message delivered to ONE consumer)
XREADGROUP GROUP order-processors worker-1 COUNT 10 BLOCK 5000 STREAMS orders >

# Acknowledge processed messages
XACK orders order-processors 1234567890-0

# Check pending (unacknowledged) messages
XPENDING orders order-processors

# Claim stuck messages (consumer died without ACK)
XAUTOCLAIM orders order-processors worker-2 60000 0  # Claim messages idle > 60s

8. Operational Best Practices

Practice Why How
Always set TTLsPrevents unbounded memory growthSETEX / EXPIRE on every key
Namespace keysAvoid collisions, enable monitoringservice:entity:id (e.g., "api:user:123")
Avoid KEYS commandBlocks Redis (O(N) full scan)Use SCAN with cursor instead
Pipeline commandsReduces round trips (10x throughput)redis.pipeline() for batch operations
Monitor memoryOOM kills Redis silentlySet maxmemory + eviction policy
Use Lua for atomicsMulti-step operations need atomicityEVAL with Lua scripts
What We've Learned Running Redis: Set maxmemory-policy to allkeys-lru for caches (evict least-recently-used when full) and noeviction for session stores or queues (reject writes instead of losing data). Monitor the used_memory metric and alert at 80% capacity. Enable RDB snapshots for persistence — AOF gives you better durability but costs more I/O. For production, use Redis Sentinel or Redis Cluster for high availability.

Frequently Asked Questions

Redis vs Memcached — when to use each?

Redis for almost everything — it has data structures, persistence, pub/sub, scripting, and clustering. Memcached only if you need multi-threaded performance for pure key-value caching at extreme scale and don't need any data structures. In practice, Redis is the right default choice in 2026.

How much memory does Redis need?

Depends on your data. A rough estimate: each key-value pair uses about 100 bytes of overhead plus the actual data size. 1 million simple cached objects (~500 bytes each) needs about 600MB. Use redis-cli INFO memory to see actual usage. Always leave 25-30% headroom for fragmentation and background operations.

Is Redis persistent? Can I use it as a primary database?

Redis has persistence (RDB snapshots + AOF logs), but it's not designed as a primary database. It lacks complex queries, joins, and strong transactional guarantees. Use Redis as a secondary store (cache, queue, session store) alongside a primary database like PostgreSQL. Exception: Redis can be primary for ephemeral data like sessions or rate limits where data loss is acceptable.

Redis Cluster or Redis Sentinel?

Sentinel for high availability without sharding (automatic failover, same dataset on all nodes). Cluster for horizontal scaling (data sharded across nodes, larger datasets). Most applications should start with Sentinel. Move to Cluster when your data doesn't fit in a single Redis instance (typically > 25GB) or you need more than 100K operations/second.

What's the best eviction policy?

allkeys-lru for general caching — evicts least recently used keys when memory is full. volatile-lru if you mix cached (with TTL) and persistent (no TTL) data. noeviction for queues and sessions where losing data is worse than rejecting writes. Never use allkeys-random — it evicts popular keys as readily as cold ones.

Pillai Infotech LLP

We implement Redis caching and real-time data patterns for high-traffic applications. Let's optimize your application's performance.

Related Articles

Database Scaling Strategies: Sharding, Replication, and Caching → NoSQL Databases Guide: MongoDB, Redis, Cassandra, and DynamoDB → Real-Time Data Processing: Kafka, Flink, and Stream Architecture →