Redis Memory Optimization
Why Memory Optimization Matters
Redis performance depends on keeping data in RAM. When memory fills, eviction policies delete keys or writes fail — both impact application behavior. Memory is often the primary scaling constraint before CPU or network.
Goals:
- Fit more data per node (delay Cluster expansion)
- Reduce cost (smaller instances)
- Prevent unexpected evictions
- Maintain predictable latency
INFO memory
MEMORY STATS
MEMORY DOCTOR
Understanding Memory Usage
| Metric | Description |
|---|---|
used_memory |
Redis allocator view |
used_memory_rss |
OS physical memory |
used_memory_dataset |
Approximate user data size |
used_memory_overhead |
Keys, metadata, buffers |
mem_fragmentation_ratio |
RSS / used_memory |
Healthy fragmentation ratio: 1.0–1.5. Above 1.5 suggests wasted RAM — consider restart or MEMORY PURGE.
MEMORY USAGE user:1001
MEMORY USAGE user:1001 SAMPLES 5
OBJECT ENCODING user:1001
Key Design for Memory Efficiency
Shorter Keys
Key names consume memory — every byte in the key is stored:
# Bad — 40+ byte key names × millions of keys
SET application:production:users:1001:profile:data "..."
# Good — concise but readable
SET u:1001:prof "..."
Rule of thumb: shorter keys save megabytes at millions of keys scale.
Right Data Structure
| Scenario | Memory-Efficient Choice |
|---|---|
| Object with fields | Hash (listpack encoding for small) |
| Unique IDs | Set (intset for integer-only small sets) |
| Approximate UV count | HyperLogLog (~12 KB fixed) |
| Boolean flags for millions of IDs | Bitmap |
| JSON blob read as whole | String with compression |
# HyperLogLog: ~12 KB vs Set: ~8 bytes × N members
PFADD uv:2024-06-13 user:1001 user:1002
PFCOUNT uv:2024-06-13
Hash Field Limits for Listpack Encoding
Small hashes use compact listpack encoding:
hash-max-listpack-entries 512
hash-max-listpack-value 64
Keep hash fields small and count ≤512 for optimal encoding. Check with OBJECT ENCODING.
Compression
Compress large string values before storing:
import zlib
import json
def set_compressed(r, key, data, ttl=3600):
compressed = zlib.compress(json.dumps(data).encode(), level=6)
r.setex(key, ttl, compressed)
def get_compressed(r, key):
raw = r.get(key)
if raw:
return json.loads(zlib.decompress(raw))
return None
Trade-off: CPU for compression/decompression vs RAM savings. Worth it for values > 1 KB.
TTL and Key Lifecycle
Keys without TTL grow memory indefinitely:
# Find keys without expiry (sample)
redis-cli --scan --pattern 'cache:*' | head -100 | while read k; do
ttl=$(redis-cli TTL "$k")
[ "$ttl" = "-1" ] && echo "$k"
done
Set TTL at creation. Audit periodically for orphan keys.
Eviction Policy Selection
maxmemory 8gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10
| Workload | Recommended Policy |
|---|---|
| Pure cache | allkeys-lru or allkeys-lfu |
| Cache + TTL sessions | volatile-lru on shared instance, or separate instances |
| No data loss tolerance | noeviction + alerting at 80% memory |
LFU (Least Frequently Used) retains hot keys better than LRU for skewed access patterns.
Data Type Specific Optimizations
Strings
# Store integers as strings — Redis optimizes int encoding
SET counter:views 1000000
OBJECT ENCODING counter:views
# "int"
Sets
Integer-only sets with ≤512 members use intset encoding:
SADD user:ids:batch1 1001 1002 1003
OBJECT ENCODING user:ids:batch1
# "listpack" or "intset"
Sorted Sets
zset-max-listpack-entries 128
zset-max-listpack-value 64
Small ZSETs use listpack — much smaller than skip list.
Streams
Trim streams to prevent unbounded growth:
XADD events * field value
XTRIM events MAXLEN ~ 50000
Fragmentation Management
High mem_fragmentation_ratio wastes RAM:
INFO memory
# mem_fragmentation_ratio:1.8
MEMORY PURGE # Redis 4+ — ask allocator to release pages
# Or planned restart during maintenance window
Causes: frequent updates/deletes, RDB forks, changing value sizes.
Prevention:
- Avoid frequent size-changing updates on same keys
- Use consistent value sizes where possible
- Restart during maintenance if ratio sustained > 1.5
Active Defragmentation (Redis 4+)
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
Redis actively defragments during idle cycles — monitor CPU impact.
Capacity Planning
# Estimate memory for N keys
redis-cli DEBUG POPULATE 1000000 user: 10
INFO memory
# used_memory / 1000000 = bytes per key (approximate)
DEBUG POPULATE 0 # clean up test data
Plan headroom:
- 20% buffer below maxmemory for AOF rewrite / RDB fork copy-on-write
- Monitor
used_memory_peakafter traffic spikes
Best Practices
- Audit key patterns — remove unused prefixes
- Set TTL on all ephemeral data
- Use hashes over JSON strings for small objects
- Use HyperLogLog/bitmaps for analytics when exact counts unnecessary
- Separate instances by workload (cache vs sessions) with appropriate policies
- Monitor memory weekly — trend analysis predicts capacity needs
Common Mistakes
| Mistake | Impact |
|---|---|
| Storing full API responses with redundant data | 10× memory vs normalized hashes |
| No maxmemory set | Host OOM |
| Long descriptive key names at scale | Millions of wasted bytes |
| Keeping debug/test keys in production | Silent memory drain |
| allkeys-lru on session store | Users logged out randomly |
Troubleshooting
Memory growing despite TTL:
INFO keyspace
# Compare keys over time
redis-cli --bigkeys
# Find largest keys
Sudden memory spike:
INFO memory
# Check used_memory_rss vs used_memory — fork COW during BGSAVE?
LASTSAVE
INFO persistence
Evictions increasing:
INFO stats
# evicted_keys counter rising
# Increase maxmemory, optimize keys, or add nodes
Performance Tips
- Run
redis-cli --bigkeysandredis-cli --memkeys(Redis 4+) monthly - Pipeline
UNLINK(notDEL) when bulk-deleting large keys - Prefer
SCAN+ batch delete overFLUSHDBin production - Use Redis 7 client-side caching (tracking) to reduce duplicate data in app + Redis
Production Scenario
A SaaS platform’s 16 GB Redis instance hit 94% memory with evictions impacting cache hit ratio (91% → 74%). Analysis via --bigkeys found 2M session keys averaging 4 KB (stored full user objects). Migration: hashes with user_id + role only (avg 180 bytes), gzip on cached API responses > 2 KB. Memory dropped to 6.2 GB. Hit ratio recovered to 93%. Deferred Cluster migration by 8 months, saving $4K/month.
Memory optimization is continuous — measure per-key cost, choose structures deliberately, and audit key lifecycles before adding hardware.