Redis reduces database load by 60-90% when implemented with the right caching pattern for your access profile. Cache-aside works for read-heavy workloads, write-through ensures consistency, and write-behind optimizes write performance. This guide covers each pattern with production configurations, TTL strategies, eviction policies, and high-availability architectures.
The 2025 Redis Labs benchmark report shows that properly configured Redis caching reduces P99 API latency from 250ms to under 5ms for cached responses, while cutting database query volume by an average of 78%. The most common production mistake is choosing the wrong caching pattern, which leads to stale data, cache stampedes, or unnecessary complexity.
---
Cache-Aside Pattern (Lazy Loading)
Cache-aside is the most common pattern. The application checks Redis first, falls back to the database on miss, and populates the cache after reading from the database.
import redis
import json
import asyncpg
redis_client = redis.Redis(host="localhost", port=6379, db=0)
async def get_user(user_id: int):
# Step 1: Check cache
cache_key = f"user:{user_id}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# Step 2: Cache miss - query database
conn = await asyncpg.connect(dsn="postgresql://localhost/myapp")
row = await conn.fetchrow(
"SELECT id, email, name FROM users WHERE id = $1", user_id
)
await conn.close()
if row is None:
return None
# Step 3: Populate cache with TTL
user_data = dict(row)
redis_client.setex(cache_key, 3600, json.dumps(user_data))
return user_data
| Aspect | Cache-Aside Behavior |
| Read path | Cache first, then DB on miss |
| Write path | Write to DB, invalidate cache |
| Consistency | Eventually consistent (TTL-bounded) |
| Cache population | On-demand (lazy) |
| Best for | Read-heavy workloads (80%+ reads) |
| Risk | Cache stampede on cold start |
---
Write-Through Pattern
Write-through writes to both the cache and database simultaneously. The cache is always up to date, eliminating stale reads at the cost of higher write latency.
async def update_user(user_id: int, name: str, email: str):
# Step 1: Write to database
conn = await asyncpg.connect(dsn="postgresql://localhost/myapp")
await conn.execute(
"UPDATE users SET name = $1, email = $2 WHERE id = $3",
name, email, user_id
)
await conn.close()
# Step 2: Update cache immediately
cache_key = f"user:{user_id}"
user_data = {"id": user_id, "name": name, "email": email}
redis_client.setex(cache_key, 3600, json.dumps(user_data))
return user_data
---
Write-Behind Pattern (Write-Back)
Write-behind writes to the cache first and asynchronously flushes to the database. This provides the lowest write latency but risks data loss if Redis crashes before flushing.
import time
import threading
WRITE_BUFFER_KEY = "write_buffer:users"
def buffer_user_update(user_id: int, data: dict):
"""Queue write in Redis, flush to DB asynchronously."""
redis_client.hset(f"user:{user_id}", mapping=data)
redis_client.sadd(WRITE_BUFFER_KEY, user_id)
def flush_write_buffer():
"""Background worker flushes buffered writes to DB."""
while True:
pending = redis_client.smembers(WRITE_BUFFER_KEY)
for user_id in pending:
user_id = int(user_id)
data = redis_client.hgetall(f"user:{user_id}")
# Write to database
sync_to_database(user_id, data)
redis_client.srem(WRITE_BUFFER_KEY, user_id)
time.sleep(5) # Flush every 5 seconds
<h1>Start background flusher</h1>
threading.Thread(target=flush_write_buffer, daemon=True).start()
| Pattern | Write Latency | Read Consistency | Data Loss Risk | Complexity |
| Cache-aside | DB latency | Eventually consistent | None | Low |
| Write-through | DB + cache latency | Strongly consistent | None | Medium |
| Write-behind | Cache latency only | Strongly consistent | If Redis crashes | High |
---
TTL Strategies
TTL (time-to-live) determines how long cached data remains valid. The right TTL balances freshness against cache hit rate.
<h1>Redis TTL commands</h1>
<h1>Set key with 1-hour TTL</h1>
SET user:123 '{"name":"Alice"}' EX 3600
<h1>Set TTL on existing key</h1>
EXPIRE user:123 3600
<h1>Check remaining TTL</h1>
TTL user:123
<h1>Remove TTL (persist forever)</h1>
PERSIST user:123
| Data Type | Recommended TTL | Reasoning |
| User profile | 1 hour | Changes infrequently, stale data is low risk |
| Session data | 24 hours | Matches session expiration |
| Product catalog | 15 minutes | Prices and availability change moderately |
| API rate limits | 60 seconds | Must be accurate for enforcement |
| Search results | 5 minutes | Balances freshness with performance |
| Configuration | 30 seconds | Critical settings need fast propagation |
---
Eviction Policies
When Redis reaches its memory limit, the eviction policy determines which keys are removed.
<h1>Set max memory and eviction policy in redis.conf</h1>
maxmemory 2gb
maxmemory-policy allkeys-lru
<h1>Or set at runtime</h1>
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
| Policy | Behavior | Best For |
| noeviction | Returns error when full | Critical data, no cache |
| allkeys-lru | Evicts least recently used | General purpose caching |
| allkeys-lfu | Evicts least frequently used | Skewed access patterns |
| volatile-lru | LRU among keys with TTL | Mixed persistent + cache |
| volatile-ttl | Evicts shortest TTL first | Time-sensitive data |
| allkeys-random | Random eviction | Uniform access patterns |
For most production workloads, `allkeys-lru` is the right default. Switch to `allkeys-lfu` if your access pattern is heavily skewed (10% of keys serve 90% of requests).
---
Redis Cluster vs Sentinel
Redis Sentinel (High Availability)
Sentinel provides automatic failover for a single-master Redis setup. Use Sentinel when your dataset fits in a single Redis instance (under 25 GB) and you need automatic failover.
<h1>sentinel.conf</h1>
sentinel monitor mymaster 10.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1
Redis Cluster (Horizontal Scaling)
Redis Cluster distributes data across multiple masters using hash slots. Use Cluster when your dataset exceeds what a single instance can hold or when you need write throughput beyond a single node.
<h1>Create a 6-node cluster (3 masters, 3 replicas)</h1>
redis-cli --cluster create \
10.0.0.1:6379 10.0.0.2:6379 10.0.0.3:6379 \
10.0.0.4:6379 10.0.0.5:6379 10.0.0.6:6379 \
--cluster-replicas 1
<h1>Check cluster health</h1>
redis-cli --cluster check 10.0.0.1:6379
| Feature | Sentinel | Cluster |
| Data distribution | Single master | Sharded across masters |
| Max dataset size | Single node limit | Sum of all masters |
| Write throughput | Single master | Distributed writes |
| Failover | Automatic (10-30s) | Automatic (10-30s) |
| Multi-key operations | Full support | Same hash slot only |
| Operational complexity | Low | Medium-High |
For detailed database caching architecture, see our database connection pooling guide. If you are evaluating whether Redis is the right caching layer for your stack, our MongoDB vs PostgreSQL comparison covers when built-in database caching is sufficient. Teams running Kubernetes can automate Redis deployment using the patterns in our Kubernetes autoscaling guide.
---
Frequently Asked Questions
How much memory should I allocate to Redis?
Allocate no more than 75% of available system memory to Redis. Redis needs overhead for background saves (fork + copy-on-write), client buffers, and internal data structures. A server with 8 GB RAM should set maxmemory to 6 GB. Monitor memory usage with `redis-cli INFO memory` and set alerts at 80% of maxmemory.
How do I prevent cache stampede?
Cache stampede occurs when a popular key expires and hundreds of concurrent requests hit the database simultaneously. Three solutions: (1) Use a lock (SETNX) so only one request rebuilds the cache while others wait. (2) Refresh popular keys before expiration using a background worker. (3) Use probabilistic early expiration where each request has a small chance of refreshing the key before TTL.
Should I use Redis or Memcached?
Redis in almost all cases. Redis supports data structures (lists, sets, sorted sets, hashes), persistence, replication, Lua scripting, and pub/sub. Memcached only advantage is slightly lower memory overhead per key for simple string caching. The operational simplicity of running one caching system (Redis) outweighs the marginal memory savings of Memcached.
Can Redis be used as a primary database?
Redis can persist data to disk (RDB snapshots + AOF logs), but it is not designed as a primary database. It lacks SQL, complex queries, and referential integrity. Use Redis for caching, session storage, rate limiting, queues, and real-time leaderboards. Keep your primary data in PostgreSQL or MongoDB.
How do I monitor Redis in production?
Run `redis-cli INFO` for real-time metrics. Key metrics to monitor: used_memory vs maxmemory, keyspace_hits vs keyspace_misses (cache hit rate), connected_clients, instantaneous_ops_per_sec, and rdb_last_bgsave_status. Export these to Prometheus with redis_exporter and create Grafana dashboards with alerts at 90% memory usage and cache hit rate below 80%.