--- title: "Redis Best Practices" description: "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." language: null framework: null category: database tags: - best-practices - database - redis - caching - nosql - performance - pub-sub - data-structures - anti-patterns keywords: - redis data structures string hash list set sorted set stream - cache-aside vs write-through caching pattern - redis eviction policy maxmemory-policy - redis rdb vs aof persistence - redis pub/sub vs streams - redis cluster basics - redis as primary datastore risk - redis 8 license agplv3 last_updated: 2026-07-07 difficulty: intermediate version: "Redis >= 8.0 (Redis Open Source, tri-licensed RSALv2/SSPLv1/AGPLv3)" related: - ../../sql/general_sql_patterns.md - ../../sql/postgresql/best_practices.md - ../mongodb/best_practices.md - ../../../languages/python/frameworks/fastapi/best_practices.md - ../../../languages/python/frameworks/django/best_practices.md - ../../../core/architecture/microservices.md search_priority: high status: published --- # Redis Best Practices ## 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 `INCR` and 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` | ```redis # 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. ```python # 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. ```python # 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. ```conf # 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 | ```conf # 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. ```redis # 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](../../../core/architecture/microservices.md) 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](../../sql/postgresql/best_practices.md) or [MongoDB Best Practices](../mongodb/best_practices.md)) 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 `maxmemory` to 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`/`XCLAIM` correctly 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`/`EXEC` or 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 *`, unbounded `SORT`, and `LRANGE 0 -1` on very large lists block the single-threaded event loop for all clients; use `SCAN` (cursor-based, non-blocking) instead of `KEYS`. - **Authentication and network exposure**: always set `requirepass` or 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_memory` versus `maxmemory` and 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}:profile` and `{user:42}:sessions` land 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`/`FUNCTION` execute 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-side `WATCH`/`MULTI`/`EXEC` transaction. - **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, `ZREMRANGEBYSCORE` to expire old entries, and `ZCARD` to 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**: `WATCH` a key, then `MULTI`/`EXEC`; the transaction aborts if the watched key changed since the `WATCH`, 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](../../sql/general_sql_patterns.md) - the relational store Redis typically caches in front of. - [PostgreSQL Best Practices](../../sql/postgresql/best_practices.md) - pairing Redis cache-aside with a Postgres source of truth. - [MongoDB Best Practices](../mongodb/best_practices.md) - document-store alternative for data that outgrows Redis's memory-bound model. - [FastAPI Best Practices](../../../languages/python/frameworks/fastapi/best_practices.md) - injecting a Redis client via dependency injection and lifespan events. - [Django Best Practices](../../../languages/python/frameworks/django/best_practices.md) - Django's cache framework backed by Redis. - [Microservices Architecture](../../../core/architecture/microservices.md) - 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 `maxmemory` is 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.