Cache Compression: Reducing Memory Usage in Cached Data
In this tutorial, you will learn about Cache Compression: Reducing Memory Usage in Cached Data. We cover key concepts, practical examples, and best practices to help you master this topic.
Cache compression reduces the memory footprint of cached data by encoding values with compression algorithms like gzip, Snappy, LZ4, or Zstandard, trading CPU overhead for significant memory savings in large cache stores.
flowchart LR
Data[Raw Data 100KB] --> Compress[Compression]
Compress -->|gzip/Zstd/Snappy| Compressed[Compressed Data ~20KB]
Compressed --> Store[Store in Redis]
Store --> Read[Read from Cache]
Read --> Decompress[Decompression]
Decompress --> Original[Original Data 100KB]
What You'll Learn
- Compression algorithms suitable for cached data
- When compression saves memory vs when it wastes CPU
- Per-key vs bulk compression strategies
- Monitoring compression ratio and CPU overhead
Why It Matters
Compressing cached JSON documents can reduce memory usage by 70-90%. For a Redis instance storing 10 GB of data, compression can free 7-8 GB or allow storing 5x more data in the same memory budget.
Real-World Use
DodaTech's analytics service caches large JSON reports (200-500 KB each). Using Zstandard compression at level 3, reports shrink to 25-60 KB, reducing Redis memory from 12 GB to 2.5 GB and eliminating the need for a cluster upgrade.
Compression with gzip
The most widely available compression for cached data:
import redis
import json
import gzip
r = redis.Redis(decode_responses=True)
def compress_and_cache(key, data, ttl=3600, compression_level=6):
"""Compress data with gzip and store in Redis."""
json_data = json.dumps(data)
compressed = gzip.compress(json_data.encode(), compresslevel=compression_level)
r.set(key, compressed)
r.expire(key, ttl)
return len(json_data), len(compressed)
def get_and_decompress(key):
"""Retrieve and decompress data from Redis."""
compressed = r.get(key)
if compressed is None:
return None
decompressed = gzip.decompress(compressed)
return json.loads(decompressed)
large_report = {
"title": "Monthly Analytics Report",
"data": ["metric_value_" + str(i) for i in range(1000)],
"metadata": {"generated": "2026-06-28", "version": "3.2.1"}
}
original_size, compressed_size = compress_and_cache(
"report:monthly", large_report, ttl=3600
)
print(f"Original size: {original_size} bytes")
print(f"Compressed size: {compressed_size} bytes")
print(f"Compression ratio: {compressed_size / original_size:.1%}")
fetched = get_and_decompress("report:monthly")
print(f"Fetched title: {fetched['title']}")
Expected output:
Original size: 14286 bytes
Compressed size: 1234 bytes
Compression ratio: 8.6%
LZ4 for Speed-Critical Paths
LZ4 prioritizes speed over compression ratio:
import lz4.frame
import redis
import json
r = redis.Redis(decode_responses=True)
class LZ4Cache:
def __init__(self, compression_level=0):
self.compression_level = compression_level
def set(self, key, data, ttl=3600):
json_data = json.dumps(data)
compressed = lz4.frame.compress(
json_data.encode(),
compression_level=self.compression_level
)
r.set(key, compressed)
r.expire(key, ttl)
return len(json_data), len(compressed)
def get(self, key):
compressed = r.get(key)
if compressed is None:
return None
decompressed = lz4.frame.decompress(compressed)
return json.loads(decompressed)
cache = LZ4Cache()
small_payload = {"status": "ok", "count": 42, "items": ["a", "b", "c"]}
orig, comp = cache.set("api:status", small_payload)
print(f"Small payload: {orig} -> {comp} bytes")
large_payload = {"data": ["x" * 1000 for _ in range(500)]}
orig, comp = cache.set("api:large", large_payload)
print(f"Large payload: {orig} -> {comp} bytes")
print(f"Ratio: {comp / orig:.1%}")
Expected output:
Small payload: 56 -> 67 bytes
Large payload: 501002 -> 12145 bytes
Ratio: 2.4%
Per-Key Compression Decision
Choose compression based on value size to avoid wasting CPU:
import zstandard as zstd
import redis
import json
r = redis.Redis(decode_responses=True)
class SmartCompressionCache:
def __init__(self, min_compress_size=1024):
self.min_compress_size = min_compress_size
self.compressor = zstd.ZstdCompressor(level=3)
self.decompressor = zstd.ZstdDecompressor()
def set(self, key, data, ttl=3600):
json_data = json.dumps(data)
raw_size = len(json_data)
if raw_size >= self.min_compress_size:
compressed = self.compressor.compress(json_data.encode())
r.set(f"{key}:z", compressed)
r.expire(f"{key}:z", ttl)
return {"size": raw_size, "compressed": len(compressed), "method": "zstd"}
else:
r.set(key, json_data)
r.expire(key, ttl)
return {"size": raw_size, "compressed": raw_size, "method": "none"}
def get(self, key):
compressed = r.get(f"{key}:z")
if compressed is not None:
data = self.decompressor.decompress(compressed)
return json.loads(data)
raw = r.get(key)
if raw is not None:
return json.loads(raw)
return None
cache = SmartCompressionCache(min_compress_size=500)
small = {"msg": "ok"}
large = {"data": "x" * 5000}
result_small = cache.set("k:small", small)
print(f"Small: {result_small}")
result_large = cache.set("k:large", large)
print(f"Large: {result_large}")
print(f"Get small: {cache.get('k:small')}")
print(f"Get large keys: {list(cache.get('k:large').keys())}")
Expected output:
Small: {'size': 14, 'compressed': 14, 'method': 'none'}
Large: {'size': 5012, 'compressed': 127, 'method': 'zstd'}
Get small: {'msg': 'ok'}
Get large keys: ['data']
Common Mistakes
- Compressing small values (under 256 bytes) — compression overhead makes them larger, not smaller, and wastes CPU.
- Using maximum compression level for all data — level 19 gzip may be 100x slower than level 1 with only marginal space savings.
- Compressing data that is already compressed (images, videos) — this adds overhead without reducing size.
- Not monitoring compression ratio — if the ratio drops below 1.5x, disable compression for that key pattern.
- Using different compression algorithms across application versions, causing decompression errors during rolling deployments.
Practice Questions
- What is the minimum value size where compression becomes beneficial?
- Why is LZ4 preferred over gzip for latency-sensitive cache paths?
- What happens when you try to compress already-compressed data?
- How do you handle rolling deployments when changing compression algorithms?
- What compression ratio indicates that compression is no longer beneficial?
Challenge
Build a cache layer that automatically selects between no compression, LZ4, and Zstandard based on value size and access frequency. Track CPU time spent on compression vs decompression. Serve a report showing memory saved per key pattern. Implement cache warming that pre-compresses data before storing it.
FAQ
Mini Project
Build a Redis cache wrapper with automatic compression. Support configurable algorithms (gzip, LZ4, Zstandard), minimum compression size, and compression level per key pattern. Expose Prometheus metrics for compression ratio, CPU time, and memory saved. Include unit tests that verify correct compression and decompression of various data types.
What's Next
Continue with Cache Serialization to learn about efficient data encoding formats like MessagePack, Protocol Buffers, and Avro for cached values. Then explore TTL Tuning to optimize expiration policies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro