Redis Patterns

Redis is the in-memory data structure server that became "the swiss army knife" of backend engineering — caches, queues, rate limits, pub/sub, leaderboards, geo. The data structures and the speed (sub-millisecond, 100k+ ops/sec on a single node) make it tempting to use for everything.

It is not a database. It is not durable in the way Postgres is durable. It is not a substitute for a real message queue. Knowing where the lines are is the difference between Redis being a delight and being a footgun.

Cache-aside

The 95% use case for Redis. Application checks cache; on miss, fetches from the source-of-truth (DB), populates cache.

def get_user(id):
    cached = redis.get(f"user:{id}")
    if cached: return json.loads(cached)
    user = db.query(...)
    redis.setex(f"user:{id}", 3600, json.dumps(user))  # 1 hour TTL
    return user

Considerations:

See CachingStrategies for cache-aside vs write-through vs other patterns.

Rate limiting

Token bucket is the typical pattern. Use a Lua script for atomicity:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])

local data = redis.call('HMGET', key, 'tokens', 'last_refill')
local tokens = tonumber(data[1]) or limit
local last_refill = tonumber(data[2]) or now

local elapsed = now - last_refill
tokens = math.min(limit, tokens + elapsed * refill_rate)

if tokens < 1 then
    redis.call('HSET', key, 'tokens', tokens, 'last_refill', now)
    return 0  -- rejected
end

tokens = tokens - 1
redis.call('HSET', key, 'tokens', tokens, 'last_refill', now)
redis.call('EXPIRE', key, 3600)
return 1  -- allowed

Atomic, fast, correct. See ApiRateLimitingAlgorithms for the full pattern.

Distributed locks (with caveats)

The naive pattern:

def acquire_lock(key, timeout):
    return redis.set(key, value, nx=True, ex=timeout)

SET NX EX is atomic. If it returns OK, you have the lock; release with a script that checks the value to prevent another caller from accidentally releasing your lock.

The catch: this is not safe under all failure conditions. Martin Kleppmann's analysis (2017) and counter-arguments are worth reading. The short version:

For locks where correctness matters (financial transactions, irreversible operations), use a real consensus system (etcd, ZooKeeper, Consul). For locks where best-effort suffices (rate-limiting periodic jobs, cron singleton enforcement), Redis-based locks are fine.

Redlock (Redis's recommended distributed lock) tries to address the failover concern by acquiring on a majority of independent Redis nodes. It improves things but doesn't fully solve the GC-pause / fencing-token problem.

Rule of thumb: Redis lock for "we'd prefer not to run two of these at once but it's not catastrophic if we do." etcd / ZooKeeper for "two of these running at once would be catastrophic."

Queues (with caveats)

Redis can be a queue. Two patterns:

List-based (LPUSH / BRPOP)

LPUSH queue "job1"
BRPOP queue 30  # blocking pop, 30s timeout

Simple, fast. Good for fire-and-forget jobs where loss is tolerable.

Limitations:

Streams

XADD / XREAD / XACK — Redis 5+. Has consumer groups, message acks, pending entries lists, retention. Closer to Kafka than to a list.

XADD orders * order_id 42 status pending
XREADGROUP GROUP processors worker1 COUNT 10 STREAMS orders >
XACK orders processors <message-id>

Production-ready for moderate workloads. Above ~10k messages/sec sustained, Kafka starts winning on durability and operational maturity.

When to use Redis for queueing:

When not:

Pub/sub

PUBLISH / SUBSCRIBE:

SUBSCRIBE notifications:user:42
PUBLISH notifications:user:42 "{"type":"new_message","id":12345}"

Fire-and-forget. Subscribers receive messages only while connected. Disconnected subscriber misses everything that was published while disconnected.

Use for:

Don't use for:

Counters and analytics

Atomic counters at speed:

INCR pageview:home:2026-04-25
INCRBYFLOAT revenue:total 19.99
HINCRBY user:42:counters orders 1

Hyperloglog for cardinality estimates (unique-visitor counts):

PFADD daily_visitors "user_42" "user_43" ...
PFCOUNT daily_visitors  -- approximate; uses 12KB regardless of count

Sorted sets for leaderboards:

ZADD leaderboard 9870 "alice" 8420 "bob"
ZREVRANGE leaderboard 0 9 WITHSCORES  -- top 10
ZREVRANK leaderboard "alice"          -- their rank

This is where Redis is almost magically efficient. Real-time analytics with millions of events / second, no specialized analytics database.

Sessions

Cache user session data with a TTL keyed by session ID. Simple, fast, scales. Most web frameworks have built-in Redis session store backends.

Caveat: if Redis is your only session store and Redis goes down, all users are logged out. Persist sessions to a durable store too if this matters.

Geo

GEOADD, GEORADIUS, GEOSEARCH. Stores lat/lng for keys; queries within radius or rectangle.

Useful for "find users near me," store locator, ride-sharing matches. Doesn't replace PostGIS for serious GIS but covers a lot of ground.

Cluster vs single node

Single Redis node: ~100k ops/sec sustained. Adequate for most applications.

Redis Cluster: shards data across nodes. Adds:

Most teams under 50k ops/sec with < 100GB working set don't need cluster. Promote when you do.

Persistence

Redis offers two modes:

For cache-only Redis: persistence off (the cache is recoverable). For Redis-as-database: AOF with everysec and replicas.

If you're using Redis as the source of truth for important data, reconsider. Redis is designed as a cache; durability is a backstop, not the focus.

Redis-compatible alternatives

By 2026, the Redis ecosystem has split. The original Redis Inc. moved to a non-OSS license; the community forked into:

For a new deployment in 2026, Valkey is the most common pick. Functionally equivalent to Redis for the patterns above.

Common mistakes

Further reading