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.

```python

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:

- **TTL is your friend.** Without a TTL, stale data lives forever in cache.

- **Invalidate on writes.** When you update the row, also `DELETE` the cache key.

- **Stampede protection.** When a popular cache key expires, many requests miss simultaneously and hit the DB. Mitigate with stale-while-revalidate (return stale; refresh asynchronously) or a single-flight pattern (one request fetches; others wait).

- **Cache stampede on cold start.** A new cache (or after a Redis restart) has 0% hit rate; the DB sees full traffic. Pre-warm critical keys; rate-limit; have headroom on the DB.

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:

```lua

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:

```python

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:

- If the Redis primary fails and the replica becomes primary before the SET propagates, two clients can hold the "lock" simultaneously.

- Network partitions cause false-but-still-running lock holders to lose mutex without knowing.

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:

- No ack / retry. Once popped, the job is gone — even if the worker crashes mid-processing.

- No persistence guarantees if Redis crashes mid-write.

- No consumer groups (everyone reads from the same queue).

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:

- **Job queues with cheap re-execution.** Background work that's idempotent and fine to lose occasionally.

- **Streams for moderate-volume durable messaging** when adding Kafka would be overkill.

- **Real-time pub/sub fan-out.**

When not:

- **Mission-critical durable messaging.** Use Kafka, RabbitMQ, SQS.

- **High volume with strict ordering.** Kafka.

- **Complex workflow orchestration.** Temporal, Airflow.

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:

- Real-time UI updates within an active session.

- Cross-instance cache invalidation.

- Loose-coupling notifications inside one application.

Don't use for:

- Persistent messaging (use Streams).

- Inter-service communication where messages must not be lost.

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:

- More memory (sum across nodes).

- More throughput (sum across nodes).

- Operational complexity.

- Limitation: multi-key operations require keys to be in the same hash slot (use hash tags `{user42}:orders` to colocate).

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

Persistence

Redis offers two modes:

- **RDB snapshots** — periodic full snapshots. Fast restart; can lose minutes of data.

- **AOF (Append-Only File)** — every write logged. Slower; near-zero data loss with `fsync=always` (rarely used; usually `everysec`).

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:

- **Valkey** (Linux Foundation fork) — drop-in replacement; community-driven; main fork most large users moved to.

- **KeyDB** — multithreaded fork; faster on large machines.

- **Dragonfly** — alternative implementation; multithreaded, claims much higher throughput.

- **Garnet** (Microsoft) — alternative implementation; fast.

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

Common mistakes

- **No TTLs.** Keys live forever; memory grows. Set TTLs on most keys.

- **Big keys (>1MB).** Slow operations, memory fragmentation. Split or store outside Redis.

- **`KEYS` in production.** O(N) blocks the entire server. Use `SCAN`.

- **Sync calls during page load.** Even sub-millisecond Redis adds up across many calls. Pipeline or batch.

- **Treating Redis as durable when persistence isn't tuned.** Default config can lose minutes of data on crash.

- **Connection pool too small.** Threads block waiting for a Redis connection while Redis is idle. Tune the pool.

Further reading

- [CachingStrategies]() — broader caching patterns

- [ApiRateLimitingAlgorithms]() — rate-limiting algorithms in detail

- [ConsistentHashing]() — distributed sharding mechanics

- [DistributedComputingAlgorithms]() — distributed primitives