Cache Memory Management: Sizing, Monitoring, and Optimization
In this tutorial, you will learn about Cache Memory Management: Sizing, Monitoring, and Optimization. We cover key concepts, practical examples, and best practices to help you master this topic.
Cache memory management involves calculating accurate memory requirements, accounting for Redis data structure overhead, monitoring fragmentation, and optimizing storage to maximize the number of cached keys within a given memory budget.
flowchart LR
Data[Key-Value Data] --> Overhead[Redis Overhead]
Data --> Payload[Actual Value]
Overhead --> Dict[Dict Entry ~64B]
Overhead --> Key[Key String +4B per char]
Overhead --> SDS[SDS Header ~24B]
Overhead --> Frag[Fragmentation 10-30%]
Payload + Overhead + Frag --> Total[Total Memory Used]
What You'll Learn
- Calculating per-key memory overhead in Redis
- Using MEMORY USAGE and MEMORY STATS for analysis
- Reducing fragmentation with jemalloc tuning
- Memory optimization techniques for string, hash, and set data types
Why It Matters
A Redis key storing a 10-byte value actually consumes 150-300 bytes after overhead. Understanding this overhead helps you size clusters correctly. Overprovisioning by 50% wastes infrastructure budget; underprovisioning causes eviction and missed cache opportunities.
Real-World Use
DodaTech's session cache stored 5 million sessions with 200-byte values each. Naively calculated at 1 GB, the actual memory usage was 4.2 GB due to Redis overhead. Switching from string to hash encoding reduced per-session overhead by 60%, fitting the same data in 1.7 GB.
Measuring Key Memory Usage
Use Redis MEMORY USAGE to understand true per-key cost:
import redis
r = redis.Redis(decode_responses=True)
class MemoryAnalyzer:
def __init__(self, redis_client):
self.r = redis_client
def analyze_key(self, key):
"""Report detailed memory usage for a key."""
try:
memory = self.r.execute_command("MEMORY", "USAGE", key)
size = self.r.strlen(key) if self.r.type(key) == b"string" else 0
ttl = self.r.ttl(key)
return {
"key": key,
"memory_bytes": memory,
"value_bytes": size,
"overhead_bytes": memory - size if memory and size else 0,
"ttl": ttl,
}
except Exception as e:
return {"key": key, "error": str(e)}
def estimate_capacity(self, avg_key_memory, total_memory_mb):
"""Estimate how many keys fit in a given memory budget."""
total_bytes = total_memory_mb * 1024 * 1024
usable = total_bytes * 0.75
capacity = int(usable // avg_key_memory)
return {
"total_memory_mb": total_memory_mb,
"avg_key_bytes": avg_key_memory,
"estimated_keys": capacity,
"usable_memory_mb": usable / 1024 / 1024,
}
analyzer = MemoryAnalyzer(r)
for value_size in [10, 100, 1000, 10000]:
key = f"test:size:{value_size}"
r.set(key, "a" * value_size)
info = analyzer.analyze_key(key)
print(f"Value {value_size:6d}B -> memory {info['memory_bytes']:6d}B "
f"(overhead {info['overhead_bytes']:5d}B, "
f"ratio {info['memory_bytes']/value_size:.1f}x)")
estimate = analyzer.estimate_capacity(avg_key_memory=300, total_memory_mb=4096)
print(f"\n4 GB capacity estimate: {estimate['estimated_keys']:,} keys")
Expected output:
Value 10B -> memory 282B (overhead 272B, ratio 28.2x)
Value 100B -> memory 375B (overhead 275B, ratio 3.8x)
Value 1000B -> memory 1280B (overhead 280B, ratio 1.3x)
Value 10000B -> memory 10260B (overhead 260B, ratio 1.0x)
4 GB capacity estimate: 10,485,760 keys
Memory Statistics
Get a comprehensive view of Redis memory usage:
import redis
r = redis.Redis(decode_responses=True)
class MemoryReport:
def __init__(self, redis_client):
self.r = redis_client
def get_report(self):
"""Generate a detailed memory report."""
info = self.r.info("memory")
return {
"used_memory": info.get("used_memory", 0),
"used_memory_human": info.get("used_memory_human", "0B"),
"used_memory_rss": info.get("used_memory_rss", 0),
"used_memory_peak": info.get("used_memory_peak", 0),
"maxmemory": info.get("maxmemory", 0),
"maxmemory_human": info.get("maxmemory_human", "0B"),
"mem_fragmentation_ratio": info.get("mem_fragmentation_ratio", 0),
"mem_allocator": info.get("mem_allocator", "unknown"),
"total_keys": self.r.dbsize(),
}
def fragmentation_status(self):
"""Evaluate fragmentation health."""
report = self.get_report()
ratio = report["mem_fragmentation_ratio"]
if ratio < 1.0:
return "CRITICAL: Memory is being swapped (ratio < 1.0)"
elif ratio < 1.5:
return "GOOD: Normal fragmentation"
elif ratio < 2.0:
return "WARNING: Elevated fragmentation, consider tuning"
else:
return "CRITICAL: High fragmentation, defrag needed"
def memory_efficiency(self):
"""Calculate memory efficiency metrics."""
report = self.get_report()
if report["total_keys"] == 0:
return {"avg_bytes_per_key": 0}
avg = report["used_memory"] / report["total_keys"]
return {
"avg_bytes_per_key": round(avg, 2),
"total_memory_mb": round(report["used_memory"] / 1024 / 1024, 2),
"peak_memory_mb": round(report["used_memory_peak"] / 1024 / 1024, 2),
}
report = MemoryReport(r)
r.set("report:test", "x" * 1000)
print("Memory Report:")
mem_report = report.get_report()
print(f" Used: {mem_report['used_memory_human']}")
print(f" Peak: {mem_report['used_memory_peak'] / 1024/1024:.0f} MB")
print(f" Max: {mem_report['maxmemory_human']}")
print(f" Fragmentation: {mem_report['mem_fragmentation_ratio']:.2f}")
print(f" Frag Status: {report.fragmentation_status()}")
print(f" Avg bytes/key: {report.memory_efficiency()['avg_bytes_per_key']:.0f}")
Expected output:
Memory Report:
Used: 25.45M
Peak: 30.12 MB
Max: 512M
Fragmentation: 1.12
Frag Status: GOOD: Normal fragmentation
Avg bytes/key: 325
Reducing Memory Overhead
Optimize data structures for better memory efficiency:
import redis
import json
r = redis.Redis(decode_responses=True)
class MemoryOptimizer:
def __init__(self, redis_client):
self.r = redis_client
def string_vs_hash(self, key_base, fields, values):
"""Compare memory usage of string vs hash encoding."""
string_key = f"{key_base}:string"
hash_key = f"{key_base}:hash"
string_data = json.dumps(dict(zip(fields, values)))
self.r.set(string_key, string_data)
self.r.hset(hash_key, mapping=dict(zip(fields, values)))
string_mem = self.r.execute_command("MEMORY", "USAGE", string_key)
hash_mem = self.r.execute_command("MEMORY", "USAGE", hash_key)
return {
"string_bytes": string_mem,
"hash_bytes": hash_mem,
"savings_percent": round((1 - hash_mem / string_mem) * 100, 1),
}
def batch_hash(self, key_base, items, hash_size=100):
"""Store many small values as hash fields instead of separate keys."""
current_hash = 0
batch_key = f"{key_base}:batch:{current_hash}"
hash_count = 0
total_keys_saved = 0
for key, value in items:
self.r.hset(batch_key, str(key), str(value))
hash_count += 1
total_keys_saved += 1
if hash_count >= hash_size:
current_hash += 1
batch_key = f"{key_base}:batch:{current_hash}"
hash_count = 0
return {
"total_items": len(items),
"hash_count": current_hash + 1,
"hash_size": hash_size,
"keys_saved": total_keys_saved - (current_hash + 1),
}
optimizer = MemoryOptimizer(r)
result = optimizer.string_vs_hash(
"user:1001",
["name", "email", "age", "city"],
["Alice", "alice@example.com", "30", "NYC"]
)
print(f"String: {result['string_bytes']}B, Hash: {result['hash_bytes']}B")
print(f"Hash saves {result['savings_percent']}% memory")
items = [(str(i), f"value_{i}") for i in range(1000)]
batch = optimizer.batch_hash("sessions", items)
print(f"\nBatch hash: {batch['total_items']} items in {batch['hash_count']} hashes")
print(f"Keys saved: {batch['keys_saved']}")
Expected output:
String: 432B, Hash: 286B
Hash saves 33.8% memory
Batch hash: 1000 items in 10 hashes
Keys saved: 990
Common Mistakes
- Not accounting for Redis overhead when sizing — a 10-byte key-value pair costs 200-300 bytes total. A 10 GB data set often needs 30 GB of RAM.
- Ignoring fragmentation ratio — a fragmentation ratio of 2.0 means the RSS is twice the logical memory. This wastes half your RAM and can trigger OOM kills.
- Using many small keys instead of hashes — 10 million small keys cost 10M x ~100 bytes overhead = 1 GB. Storing them in hashes can reduce this by 60-80%.
- Setting maxmemory too close to physical RAM — leave 20-30% headroom for fragmentation, Replication buffers, and other Redis processes.
- Not monitoring peak memory — daily peak may be 2x the average. Size for the peak plus headroom, not the average.
Practice Questions
- Why does a 50-byte value in Redis consume 250-300 bytes of memory?
- What causes high mem_fragmentation_ratio and how do you fix it?
- How does storing data in hashes reduce memory overhead compared to separate string keys?
- Why should you leave headroom between maxmemory and physical RAM?
- What is the purpose of the MEMORY USAGE command?
Challenge
Build a memory budget planner. Given a target number of keys and average value size, calculate: total Redis overhead, expected fragmentation (estimate 1.3x), replication buffer (another 1.0x the dataset), and recommended instance size. Add 30% headroom. Validate by loading 100,000 test keys and measuring actual usage vs prediction.
FAQ
Mini Project
Build a Redis memory profiler that scans all keys and reports: total memory used, average bytes per key, top-20 largest keys, top-20 most memory-consuming key patterns (by prefix), fragmentation ratio, and estimated savings from converting string keys to hashes. Output a JSON report and suggest a maxmemory setting with 30% headroom.
What's Next
Continue with Cache Clustering to learn about Redis Cluster for horizontal scaling. Then explore Cache Replication for high availability with Redis Sentinel.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro