Redis Best Practices
Production guidance for Redis covering core data structure selection, caching patterns (cache-aside, write-through), eviction policies, RDB vs AOF persistence trade-offs, pub/sub vs streams, and Redis Cluster fundamentals.
Introduction / Overview
Redis is the dominant in-memory data structure store in 2026, used as a cache, session store, rate limiter, message broker, and - increasingly, via its expanded native data types - a lightweight primary store for specific workloads that genuinely fit its model. Redis 8 consolidated the former "Redis Stack" modules (JSON, time series, probabilistic structures, and a new vector set type for embedding search) directly into Redis Open Source, and re-licensed the project under a tri-license (RSALv2, SSPLv1, or AGPLv3, at the user's option), restoring an OSI-approved open-source option after the 2024 licensing controversy.
The recurring failure mode this document guards against: treating Redis's speed as a substitute for understanding its durability and consistency trade-offs. Redis is exceptionally good at what it is designed for - sub-millisecond access to hot, size-bounded, mostly-ephemeral data - and a poor fit as the sole system of record for data that must never be lost.
When to use this guidance: Designing a caching layer, choosing a Redis data structure for a specific access pattern, configuring persistence or eviction for production, or evaluating whether Redis is being asked to do a relational or document database's job.
Core Concepts
- In-memory, optionally persisted: all data lives in RAM by default; persistence (RDB snapshots, AOF log, or both) is an opt-in durability mechanism layered on top, not Redis's default behavior.
- Single-threaded command execution: each Redis command (per shard) executes atomically without interleaving from other clients, which is why patterns like
INCRand Lua scripts provide race-free semantics without explicit application-level locking. - TTL-driven expiry: nearly every key can carry an expiration; Redis is designed around data that is allowed to disappear, which is central to using it safely as a cache rather than a database.
- Data structures as first-class citizens: unlike a plain key-value store, Redis exposes rich structures (hashes, sorted sets, streams, and more) with atomic operations on them, which is what makes Redis useful for far more than caching alone.
Best Practices
1. Choose the Data Structure That Matches the Access Pattern
Rationale: Picking the wrong structure (e.g., storing a serialized JSON blob in a plain string when a hash would allow partial field updates) forces read-modify-write round trips and loses Redis's atomic per-field operations.
| Structure | Use when | Key operations |
|---|---|---|
| String | Simple values, counters, serialized blobs, cache entries | SET, GET, INCR, SETEX |
| Hash | An object with multiple fields you update independently | HSET, HGET, HINCRBY, HGETALL |
| List | Ordered sequences, simple queues (FIFO/LIFO) | LPUSH, RPOP, LRANGE, BLPOP |
| Set | Unique, unordered membership; set algebra (union/intersect) | SADD, SISMEMBER, SINTER |
| Sorted Set (ZSet) | Ranked/scored data: leaderboards, rate-limiting windows, priority queues | ZADD, ZRANGE, ZINCRBY, ZRANGEBYSCORE |
| Stream | Append-only event log with consumer groups, replay, and acknowledgment | XADD, XREADGROUP, XACK, XCLAIM |
| JSON (native since Redis 8) | Nested documents requiring partial updates/path queries | JSON.SET, JSON.GET, JSON.MGET |
| Probabilistic (Bloom/Cuckoo/Count-Min/Top-K/t-digest) | Approximate membership, frequency, or quantile estimation at massive scale with bounded memory | BF.ADD, CMS.INCRBY, TOPK.ADD, TDIGEST.QUANTILE |
| Vector set (Redis 8) | High-dimensional embedding storage and similarity search | VADD, VSIM |
# Hash: partial field updates without read-modify-write races
HSET session:abc123 user_id 42 last_seen 1751880000 cart_items 3
HINCRBY session:abc123 cart_items 1
# Sorted set: atomic leaderboard update and ranked read
ZADD leaderboard:weekly 1500 "player:42"
ZREVRANGE leaderboard:weekly 0 9 WITHSCORES # top 10
# Stream: durable, replayable event log with consumer groups
XADD orders:events '*' order_id 9001 status "created"
XREADGROUP GROUP order-workers worker-1 COUNT 10 STREAMS orders:events '>'
Why this works well in production: sorted sets give you an atomic, server-side-ranked leaderboard or sliding-window rate limiter without shipping the full dataset to the client for sorting; streams give you at-least-once delivery semantics that plain pub/sub cannot.
2. Use Cache-Aside for Most Read-Heavy Workloads, Write-Through Where Staleness Is Unacceptable
Rationale: Cache-aside (lazy loading) keeps the cache as a pure optimization layer that can be wiped and rebuilt safely; write-through guarantees the cache is never stale but couples every write path to the cache's availability.
# Cache-aside pattern
async def get_user(user_id: int) -> User:
cached = await redis.get(f"user:{user_id}")
if cached is not None:
return User.model_validate_json(cached)
user = await db.fetch_user(user_id) # source of truth: Postgres
await redis.set(f"user:{user_id}", user.model_dump_json(), ex=300)
return user
async def update_user(user_id: int, data: UserUpdate) -> None:
await db.update_user(user_id, data)
await redis.delete(f"user:{user_id}") # invalidate, don't update-in-place
Why invalidate rather than update on write: updating the cache directly on every write risks a race where a slow read populates a stale value after the invalidation, re-introducing staleness. Deleting is simpler and cheaper, at the cost of one cache miss on the next read - an acceptable trade for most workloads.
# Write-through: cache updated synchronously with the source of truth,
# appropriate when read-after-write consistency from the cache is required
async def update_price(sku: str, price: Decimal) -> None:
await db.update_price(sku, price)
await redis.set(f"price:{sku}", str(price))
3. Configure an Explicit Eviction Policy - Never Rely on the Default for a Cache Workload
Rationale: The default noeviction policy returns errors on writes once maxmemory is reached, which is correct for a primary-store workload but catastrophic for a cache that is expected to fill up under normal operation.
# redis.conf - cache-oriented configuration
maxmemory 4gb
maxmemory-policy allkeys-lru # evict any key using LRU approximation when full
| Policy | Behavior | Use when |
|---|---|---|
noeviction | Reject writes when full | Redis holds data that must never be silently dropped |
allkeys-lru | Evict least-recently-used key, any key | General-purpose cache, no TTLs required on every key |
allkeys-lfu | Evict least-frequently-used key | Workloads with a stable "hot set" that should resist eviction despite recency |
volatile-lru / volatile-lfu | Only evict keys with a TTL set | Mixed workload: some keys are cache (evictable), others are durable (protected) |
volatile-ttl | Evict the key closest to expiring | Cache where "about to expire anyway" keys should go first |
4. Understand RDB vs AOF Before Choosing (or Combining) Them
Rationale: RDB and AOF make different durability/performance trade-offs; production deployments that need real durability guarantees typically run both, not either exclusively.
| Aspect | RDB (snapshot) | AOF (append-only file) |
|---|---|---|
| Mechanism | Point-in-time binary snapshot on a schedule or on demand | Every write command logged, replayed on restart |
| Durability | Data since the last snapshot is lost on crash | Configurable: always (fsync every write, safest, slowest), everysec (default, ~1s window), no |
| Restart speed | Fast (load one compact file) | Slower (replay the log, though rewrite/compaction mitigates this) |
| File size | Compact | Larger, though appendonly rewrite compacts periodically |
| Recommended for | Backups, fast recovery, acceptable to lose a few minutes of writes | Workloads where minimizing data loss on crash matters |
# redis.conf - combined persistence for durability-sensitive deployments
save 900 1
save 300 10
appendonly yes
appendfsync everysec
Guideline: if Redis is purely a cache rebuildable from the source of truth, persistence can reasonably be disabled entirely (save "", appendonly no) to maximize memory available for data and simplify operations - but this decision must be explicit and documented, not a default nobody examined.
5. Choose Pub/Sub for Fire-and-Forget Signaling, Streams for Anything That Must Not Be Lost
Rationale: Pub/Sub messages are delivered only to subscribers connected at the moment of publication; a disconnected consumer permanently misses messages published during the disconnect. Streams persist messages and support consumer groups with acknowledgment and replay.
# Pub/Sub: acceptable for ephemeral signals like "invalidate this cache key on all instances"
PUBLISH cache:invalidate "user:42"
# Streams: required when message loss is unacceptable (order processing, audit events)
XADD orders:events '*' order_id 9001 event "payment_confirmed"
XREADGROUP GROUP fulfillment consumer-1 COUNT 1 STREAMS orders:events '>'
XACK orders:events fulfillment 1751880000000-0
When to reach for streams over a dedicated message broker: streams are a reasonable choice when Redis is already present in the architecture and throughput/retention needs are moderate; for very high-throughput or long-retention event logs, a dedicated log-based broker (Kafka, or a managed equivalent) is usually a better fit - see Microservices Architecture for event-driven communication trade-offs between services.
Common Pitfalls & Anti-Patterns
Pitfall: Using Redis as the Sole System of Record Without Understanding Persistence Trade-offs
Problem: Teams store business-critical data (orders, financial balances, user accounts) in Redis alone because it is fast and already deployed, without configuring AOF, without understanding that a crash between snapshots loses data, and without replication for high availability.
Recommended approach: Redis can be a legitimate primary store for genuinely ephemeral or reconstructible data (sessions, rate-limit counters, real-time leaderboards), but data requiring durable, auditable guarantees belongs in a relational or document database (see PostgreSQL Best Practices or MongoDB Best Practices) with Redis layered in front as a cache.
Pitfall: Unbounded Key Growth Without TTLs
Problem: Cache keys written without an expiration accumulate indefinitely, silently consuming memory until maxmemory is hit and the configured eviction policy starts making decisions the application never anticipated.
Recommended approach: set a TTL on every cache-role key at write time (SETEX, or EXPIRE immediately after SET), and audit TTL-less keys periodically with SCAN plus TTL checks - never use KEYS * in production, as it blocks the single-threaded event loop.
Pitfall: Storing Large Blobs or Hot, Frequently-Scanned Collections as a Single Giant Key
Problem: A single hash or list with hundreds of thousands of fields/elements creates a "hot key" that concentrates load on one shard in a clustered deployment and makes operations like HGETALL or LRANGE 0 -1 expensive single-command latency spikes.
Recommended approach: shard large logical collections across multiple keys (e.g., cart:{user_id}:{shard}) or use structures designed for the access pattern (sorted sets with range queries instead of scanning entire lists).
Pitfall: Treating Cache Invalidation as an Afterthought
Problem: Updating the source of truth without invalidating (or updating) the corresponding cache entry produces stale reads that are notoriously hard to reproduce and debug.
Recommended approach: invalidate in the same code path/transaction boundary as the write, and consider a short TTL as a safety net even on data paths where invalidation logic exists, so a missed invalidation self-heals within a bounded window.
Testing Strategies
- Use a real Redis instance in integration tests (via Testcontainers or a local
redis-server) rather than mocking the client - Redis's atomicity guarantees (INCR, Lua scripts,WATCH/MULTI) are exactly the behavior most worth testing, and mocks rarely replicate them faithfully. - Test cache-aside invalidation explicitly: write a test that updates the source of truth, asserts the cache key is gone or updated, then asserts the next read returns fresh data.
- Test eviction behavior under memory pressure in a dedicated environment with a small
maxmemoryto confirm the chosen policy behaves as expected before relying on it in production sizing. - Test consumer-group failure recovery for streams: kill a consumer mid-processing and assert
XPENDING/XCLAIMcorrectly reassigns unacknowledged messages to another consumer. - Load-test rate limiters and leaderboards built on sorted sets under concurrent writers to confirm atomicity assumptions hold under real contention, not just in single-threaded test runs.
Performance, Security & Scalability Considerations
- Pipelining: batch multiple commands into a single round trip (
MULTI/EXECor client-side pipelining) whenever issuing several independent commands - network round-trip latency, not Redis's per-command execution time, is the usual bottleneck at scale. - Avoid blocking commands on the main workload path:
KEYS *, unboundedSORT, andLRANGE 0 -1on very large lists block the single-threaded event loop for all clients; useSCAN(cursor-based, non-blocking) instead ofKEYS. - Authentication and network exposure: always set
requirepassor configure Redis ACLs (per-user, per-command, per-key-pattern permissions available since Redis 6+), and never expose Redis directly to the public internet - bind to private networks and use TLS for connections crossing untrusted boundaries. - Memory sizing: monitor
used_memoryversusmaxmemoryand eviction/expired key counters (INFO stats); a cache that evicts aggressively under normal load is undersized for its working set, not merely "working as designed." - Redis Cluster shards data across multiple nodes using 16384 hash slots, enabling horizontal scaling of both memory and throughput beyond a single node, at the cost of losing cross-slot multi-key operations unless keys are deliberately co-located via hash tags (
{user:42}:profileand{user:42}:sessionsland on the same slot). - Replication and failover: pair Redis Cluster or standalone replication with Sentinel (standalone) or Cluster's built-in failover for automatic promotion of a replica on primary failure; test failover under load, not just in isolation.
Edge Cases & Advanced Usage
- Lua scripting and server-side functions:
EVAL/FUNCTIONexecute atomically alongside all other commands, making them the correct tool for check-then-act logic (e.g., "decrement inventory only if sufficient stock remains") that would otherwise require a client-sideWATCH/MULTI/EXECtransaction. - Probabilistic structures for bounded-memory approximation: a Bloom filter (
BF.ADD/BF.EXISTS) answers "have I possibly seen this before" in constant memory regardless of set size, at the cost of a tunable false-positive rate and no false negatives - appropriate for deduplication at scale where exact tracking is too memory-expensive. - Sliding-window rate limiting with sorted sets: store request timestamps as scores,
ZREMRANGEBYSCOREto expire old entries, andZCARDto count requests in the current window - an atomic, accurate alternative to fixed-window counters that avoids boundary-burst issues. Redis 8.8 additionally introduced a native window counter rate limiter, reducing the need to hand-roll this pattern. - Multi-key transactions with optimistic locking:
WATCHa key, thenMULTI/EXEC; the transaction aborts if the watched key changed since theWATCH, giving compare-and-swap semantics without a distributed lock. - Vector sets for AI workloads (Redis 8): storing and querying high-dimensional embeddings directly in Redis avoids a separate vector database for moderate-scale semantic search or recommendation use cases, though dedicated vector databases remain preferable at very large embedding-set scale or when advanced index tuning (HNSW parameter control, hybrid filtering) is required.
When Not to Use / Alternatives
| Dimension | Redis | PostgreSQL | MongoDB |
|---|---|---|---|
| Sub-millisecond read latency | Excellent | Good (with caching) | Good |
| Durable system-of-record guarantees | Requires careful AOF/replication configuration | Excellent, native | Good, with majority write concern |
| Complex relational queries/joins | Poor | Excellent | Fair (aggregation pipeline) |
| Ad-hoc analytical queries | Poor | Good | Good |
| Cost at very large data volumes | Expensive (RAM-bound) | Cheaper (disk-backed) | Cheaper (disk-backed) |
Choose PostgreSQL or MongoDB as the system of record whenever data must survive a full cluster restart with strong durability guarantees and Redis merely accelerates reads against it. Choose Redis for session storage, rate limiting, real-time leaderboards, pub/sub signaling, and caching - not as a replacement for a durable primary datastore.
Related Documentation
- General SQL Design Patterns - the relational store Redis typically caches in front of.
- PostgreSQL Best Practices - pairing Redis cache-aside with a Postgres source of truth.
- MongoDB Best Practices - document-store alternative for data that outgrows Redis's memory-bound model.
- FastAPI Best Practices - injecting a Redis client via dependency injection and lifespan events.
- Django Best Practices - Django's cache framework backed by Redis.
- Microservices Architecture - Redis streams/pub-sub as inter-service messaging options.
Glossary
- Cache-aside: A pattern where the application checks the cache first, falls back to the source of truth on a miss, and populates the cache afterward.
- Write-through: A pattern where writes update the cache and the source of truth synchronously, guaranteeing the cache is never stale.
- Eviction policy: The rule Redis applies to remove keys when
maxmemoryis reached. - RDB: A point-in-time binary snapshot persistence mechanism.
- AOF: Append-only file persistence, logging every write for replay on restart.
- Hash slot: One of 16384 fixed partitions Redis Cluster uses to distribute keys across nodes.
- Hash tag: A
{...}substring in a key used to force co-location of related keys on the same cluster hash slot. - Consumer group: A Streams feature allowing multiple consumers to cooperatively read and acknowledge a stream with at-least-once delivery.