❝ Redis is not just a cache—it's a versatile data structure server. But using it effectively requires understanding caching patterns that balance speed, consistency, and reliability. From simple key‑value lookups to advanced patterns like leaderboards, rate limiting, and distributed locks, Redis can be the backbone of your application's performance layer.❞
This guide takes you from foundational caching strategies (cache‑aside, read‑through, write‑through) to advanced patterns like cache invalidation with pub/sub, Bloom filters, and Redis modules. You'll learn how to choose the right strategy, avoid common pitfalls, and build robust, scalable systems.
Redis is designed for caching, but its features make it suitable for many secondary use cases (queues, real‑time analytics, session stores).
The most common pattern. The application first checks the cache; if not present, it loads from the database and stores the result.
def get_user(user_id):
# 1. Try cache
user = redis.get(f"user:{user_id}")
if user:
return json.loads(user)
# 2. Cache miss: fetch from DB
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. Store in cache with TTL
redis.setex(f"user:{user_id}", 3600, json.dumps(user))
return user
Pros: Simple, only caches data that is actually requested. Cons: First request is slow; cache misses can spike database load.
SETNX or a distributed lock to prevent thundering herd (multiple simultaneous cache misses for the same key).
Cache‑aside handles reads; writes require special handling to keep cache consistent.
Update cache and database atomically (or in the same transaction).
def update_user(user_id, data):
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
redis.set(f"user:{user_id}", json.dumps(data))Pros: Cache always consistent. Cons: Write latency increases.
Write to cache first, asynchronously persist to DB (using a queue).
redis.set(f"user:{user_id}", data)
queue.enqueue(update_db, user_id, data)Pros: Very fast writes. Cons: Risk of data loss if cache fails before persistence.
Keeping cache fresh is challenging. Common approaches:
# Redis Pub/Sub example for invalidation
redis.publish('cache_invalidations', f'user:{user_id}')
# In worker:
def on_message(msg):
redis.delete(msg['data'])
Redis offers several eviction policies when maxmemory is reached:
noeviction – return errors on write.allkeys‑lru – evict least recently used keys across all keys.volatile‑lru – evict LRU among keys with TTL.allkeys‑random – random eviction.volatile‑ttl – evict keys with the shortest TTL first.Choose based on data importance. For pure cache, allkeys‑lru is common. For mixed use (cache + persistent data), volatile‑lru protects keys without TTL.
Store objects as field‑value pairs. Efficient for partial updates.
HSET user:1001 name "Alice" email "alice@example.com"
HGET user:1001 nameIdeal for leaderboards, time‑series, rate limiting.
ZADD leaderboard 1000 "user1" 2000 "user2"
ZRANGE leaderboard 0 10 WITHSCORESSpace‑efficient for unique counts (e.g., daily active users).
PFADD page_views:2025-04-02 "user123"
PFCOUNT page_views:2025-04-02SETNX with TTL) to let one client populate the cache.Redis can implement distributed locks for coordinating access to shared resources. The Redlock algorithm (by Antirez) ensures safety across multiple Redis instances.
import redis
import time
import uuid
def acquire_lock(conn, lockname, acquire_timeout=10):
identifier = str(uuid.uuid4())
lock_key = f"lock:{lockname}"
end = time.time() + acquire_timeout
while time.time() < end:
if conn.set(lock_key, identifier, nx=True, ex=10):
return identifier
time.sleep(0.001)
return False
def release_lock(conn, lockname, identifier):
lock_key = f"lock:{lockname}"
# Lua script for atomic check‑and‑delete
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
return conn.eval(script, 1, lock_key, identifier)
Use locks sparingly; prefer optimistic concurrency when possible.
Redis excels at implementing rate limiters. Two common algorithms:
Simple but can allow bursts at boundaries.
key = f"rate:{user_id}"
current = redis.incr(key)
if current == 1:
redis.expire(key, 60)
if current > 100:
raise RateLimitExceeded()More accurate, tracks timestamps.
key = f"ratelimit:{user_id}"
now = time.time()
window_start = now - 60
redis.zremrangebyscore(key, 0, window_start)
count = redis.zcard(key)
if count < 100:
redis.zadd(key, {now: now})
redis.expire(key, 60)
else:
raise RateLimitExceeded()Cache penetration occurs when requests for non‑existent keys bypass cache and hit the database. A Bloom filter can quickly tell if a key probably exists, preventing expensive DB queries for absent data.
# Using RedisBloom module
redis.execute_command('BF.ADD', 'user_ids', user_id)
# Before querying cache or DB:
if not redis.execute_command('BF.EXISTS', 'user_ids', user_id):
return None # definitely not exists
Bloom filters are memory‑efficient with a tunable false‑positive rate.
Lua scripts run atomically in Redis, allowing complex operations without race conditions. Example: increment and get with TTL reset.
-- atomic_increment.lua
local key = KEYS[1]
local ttl = tonumber(ARGV[1])
local value = redis.call('INCR', key)
if value == 1 then
redis.call('EXPIRE', key, ttl)
end
return value
Use EVAL to execute. This pattern is great for counters, rate limiters, and conditional updates.
For large‑scale applications:
When using cluster, be aware of cross‑slot operations limitations.
Redis Streams can be used to propagate cache invalidations or for event sourcing. Example: broadcast invalidation events to all application instances.
# Producer
redis.xadd('cache_events', {'key': 'user:123', 'action': 'invalidate'})
# Consumer (each instance)
last_id = '0-0'
while True:
events = redis.xread({'cache_events': last_id}, block=1000)
for stream, msgs in events:
for msg_id, fields in msgs:
redis.delete(fields[b'key'])
last_id = msg_id
This ensures all nodes evict stale data without polling.
A social network needed to cache user feeds (list of post IDs). They used:
Result: 99.9% of feed reads served from Redis with < 10ms latency, handling 200k QPS.
Essential metrics:
INFO memoryINFO stats (keyspace_hits, keyspace_misses)SLOWLOG GET 10 to identify slow commands.redis-cli --latency.Set up alerts for low hit ratio, high evictions, or OOM.
Redis caching is both an art and a science. Starting with simple cache‑aside and TTL, you can evolve to sophisticated patterns like distributed locks, bloom filters, and real‑time invalidation streams. The key is to understand your data access patterns, measure, and iterate. Redis's flexibility lets you adapt as your application grows, ensuring that your users always get a blazing‑fast experience.
Happy caching — may your hit ratio be high and your latency low.