Skip to main content
Why CacheeHow It Works
All Verticals5G TelecomAd TechAI InfrastructureFraud DetectionGamingTrading
PricingDocsBlogSchedule DemoLog InStart Free Trial
← Back to Blog
Performance

How to Reduce Redis Memory Usage in Production

Your Redis instance is using 12 GB of memory on a 13 GB node. You have been upgrading instance sizes every few months, and your ElastiCache bill keeps climbing. But most of that memory is not holding useful data. It is holding bloated serialization formats, keys that should have expired weeks ago, fragmented allocator overhead, and duplicate values that could be stored once. Redis memory bloat is one of the most common — and most fixable — problems in production caching. This guide covers how to diagnose what is eating your memory, how to fix it, and how an L1 cache layer can reduce Redis memory pressure by 60–80% by keeping hot data out of Redis entirely.

40–60% Typical Waste
3–5× JSON vs. MsgPack
1.5+ Frag Ratio (Bad)
60–80% Reduction w/ L1

Why Redis Memory Bloats

Redis stores everything in memory. There is no overflow to disk, no automatic compaction, and no garbage collector that reclaims wasted space in the background. When memory usage climbs, it stays high until you actively fix the root cause. There are four primary drivers of Redis memory bloat in production, and most instances suffer from at least two of them simultaneously.

1. Keys Without Expiry

This is the single most common cause of Redis memory growth. Your application writes a key and never sets a TTL. The key persists forever. Over weeks and months, dead keys accumulate — session data for users who never returned, cached API responses for endpoints that have been deprecated, feature flags for experiments that ended six months ago. The data is useless, but Redis dutifully holds it in memory because no one told it to stop. Run DBSIZE on a production instance that has been running for a year and compare the result to the number of keys your application actually needs. The gap is often 30–50% of the total keyspace.

2. Oversized Values and Bad Serialization

Most applications serialize objects to JSON and store the result as a Redis string. JSON is human-readable but extremely wasteful for storage. A user profile object that occupies 200 bytes as a native struct becomes 800 bytes as JSON, thanks to repeated field names, string quoting, and whitespace. Multiply that by a million keys and you are wasting 600 MB of memory on serialization overhead alone. Nested objects and arrays make it worse — a JSON array of 100 product IDs includes a bracket, a comma, and whitespace for every entry. The same data in a compact binary format like MessagePack or Protocol Buffers is 3–5x smaller.

3. Memory Fragmentation

Redis uses jemalloc (or libc malloc) to manage memory. When keys are created and deleted repeatedly, the allocator ends up with small gaps between active allocations that cannot be reused for larger objects. This is memory fragmentation, and it means Redis is holding physical memory that it cannot use. A fragmentation ratio of 1.0 is perfect — every byte of RSS is holding live data. A ratio of 1.5 means Redis is using 50% more physical memory than it needs for the data it holds. Ratios above 2.0 are common on long-running instances with high churn.

4. Wrong Data Structures

Redis offers specialized data structures — Hashes, Sets, Sorted Sets, Lists — each with encoding optimizations for small cardinalities. A Hash with fewer than 128 fields and values under 64 bytes uses a ziplist encoding that is extremely compact. But if you store each field as a separate top-level String key, you pay the overhead of a full Redis object header (roughly 90 bytes) per key. Storing 100 user attributes as 100 separate keys costs ~9 KB in object headers alone. The same data in a single Hash uses under 1 KB total.

Diagnosing Memory Waste

Before optimizing, you need to know where the memory is going. Redis provides three commands that give you a complete picture.

# Overall memory breakdown redis-cli INFO MEMORY # Key fields to check: # used_memory_human: 11.82G (data + overhead) # used_memory_rss_human: 14.67G (actual RSS from OS) # mem_fragmentation_ratio: 1.24 (RSS / used_memory) # used_memory_dataset: 10.14G (actual key-value data) # used_memory_overhead: 1.68G (internal bookkeeping)

The fragmentation ratio is the first number to check. If mem_fragmentation_ratio is above 1.5, you are losing significant memory to allocator fragmentation. If used_memory_overhead is more than 15–20% of used_memory, you likely have too many small keys and should consolidate into Hashes.

# Check memory usage of a specific key redis-cli MEMORY USAGE user:123:profile # (integer) 872 <-- 872 bytes for this key # Find the largest keys (sampled scan) redis-cli --bigkeys # Scans the keyspace and reports the biggest key per type: # Biggest string: "api:cache:products" (2.4 MB) # Biggest hash: "session:abc123" (156 KB) # Get Redis memory advisor recommendations redis-cli MEMORY DOCTOR # Returns plain-text recommendations like: # "Peak memory is significantly higher than current memory..." # "High fragmentation detected..."

Run --bigkeys first to find outliers. A single 5 MB key is not unusual — it is usually a cached API response that should have been compressed or paginated. Then use MEMORY USAGE to spot-check keys that represent common patterns. If your user:*:profile keys are 800 bytes each and you have a million of them, that is 800 MB that could be 200 MB with better serialization.

The 90-byte tax: Every top-level Redis key carries approximately 90 bytes of overhead for the object header, key string, and hash table entry — regardless of the value size. If you are storing thousands of small values as individual keys, you may be spending more memory on overhead than on actual data.

Actionable Fixes

Compress Values Before Storing

Switch from JSON to a binary format like MessagePack, Protocol Buffers, or CBOR. If you must use JSON, compress it with gzip or LZ4 before writing to Redis. LZ4 compression on JSON payloads typically achieves a 4–6x size reduction with sub-millisecond encode/decode times. The CPU cost is negligible compared to the memory saved. For a million keys averaging 800 bytes in JSON, switching to LZ4-compressed MessagePack saves roughly 600–700 MB.

# Python example: compress before SET, decompress after GET import lz4.frame, msgpack, redis r = redis.Redis() # Write: serialize + compress data = {"name": "Alice", "plan": "enterprise", "features": [1,2,3,4,5]} packed = lz4.frame.compress(msgpack.packb(data)) r.setex("user:123", 3600, packed) # Read: decompress + deserialize raw = r.get("user:123") result = msgpack.unpackb(lz4.frame.decompress(raw))

Enforce TTL on Every Key

Set a TTL on every key your application writes. No exceptions. If the data is a cache entry, the TTL should match the staleness tolerance of the consuming feature. If you do not know the correct TTL, start with 24 hours and tune down. An imperfect TTL is infinitely better than no TTL. To find existing keys without expiry, use OBJECT HELP and TTL sampling, or run a SCAN loop that checks TTL for each key and flags anything returning -1 (no expiry).

Use Hashes for Small Objects

Instead of storing each user attribute as a separate key (user:123:name, user:123:email, user:123:plan), store them as fields in a single Hash (user:123). When the Hash has fewer than hash-max-ziplist-entries fields (default 128) and all values are under hash-max-ziplist-value bytes (default 64), Redis uses the ziplist encoding — a compact, contiguous memory layout that eliminates per-field overhead. This is one of the highest-impact optimizations for instances with millions of small keys.

Choose the Right Eviction Policy

If you are using Redis as a cache (not as a primary data store), set maxmemory-policy to allkeys-lfu. This evicts the least-frequently-used keys when memory pressure hits the maxmemory limit. The default policy is noeviction, which returns errors when memory is full — that is correct for a data store, but catastrophic for a cache. LFU is almost always better than LRU for cache workloads because it accounts for access frequency, not just recency. A key accessed 10,000 times yesterday but not today is more valuable than a key accessed once 30 seconds ago.

# redis.conf: configure as a bounded cache maxmemory 10gb maxmemory-policy allkeys-lfu # Or set at runtime: redis-cli CONFIG SET maxmemory-policy allkeys-lfu

Defragment Active Memory

Redis 4.0+ supports online defragmentation via activedefrag. When enabled, Redis periodically reallocates values to eliminate fragmentation gaps without downtime. Enable it with CONFIG SET activedefrag yes. Monitor mem_fragmentation_ratio after enabling — it should trend down toward 1.0 over hours. Be aware that active defrag consumes CPU, so schedule it during low-traffic windows or set conservative thresholds with active-defrag-threshold-lower and active-defrag-cycle-min.

The L1 Solution: Reduce Redis Memory Pressure by 60–80%

All of the fixes above reduce how much memory Redis uses per key. But the most effective way to reduce Redis memory usage is to reduce the number of keys Redis needs to hold. If 80% of your reads hit the same 5–10% of keys, those keys do not need to live in Redis at all. They can live in your application’s process memory, served at 1.5 microseconds with zero Redis overhead.

This is what Cachee does. It maintains an L1 in-process cache that intercepts reads before they reach Redis. Hot keys — the keys responsible for the overwhelming majority of your read traffic — live in Cachee’s local memory tier. Redis only needs to store cold data, writes, and keys that Cachee has not seen yet. The result is a dramatically smaller Redis working set.

The math is straightforward. If your Redis instance holds 10 GB of data and 80% of reads target 2 GB of hot keys, moving those hot keys to an L1 tier means Redis only needs to actively manage the remaining 8 GB. But the impact is even larger than that because the hot keys are the ones with the most churn — the most writes, the most updates, the most fragmentation. Removing them from Redis reduces fragmentation, reduces the active eviction workload, and allows you to downsize your Redis node by one or two instance sizes. That is a direct cost saving on your infrastructure bill.

With Cachee’s predictive pre-warming, the L1 hit rate reaches 100%. That means 99 out of every 100 reads never touch Redis. Redis becomes a durable backing store for cold data, not a hot-path lookup engine. Your used_memory drops, your fragmentation ratio improves, and your p99 latency drops from 1ms to 1.5 microseconds because the hot path is entirely in-process.

The compounding effect: Compressing values saves 3–5x per key. Setting TTLs reclaims 30–50% of dead keys. Consolidating into Hashes eliminates per-key overhead. And adding an L1 cache layer removes 60–80% of hot data from Redis entirely. Applied together, these optimizations can reduce a 13 GB Redis instance to under 3 GB — letting you drop two node sizes and save hundreds per month.

Further Reading

Related Reading

Also Read

Stop Wasting Memory on Hot Keys.

See how Cachee’s L1 layer moves your hottest data into process memory — cutting Redis memory usage by 60–80% and every lookup to 1.5µs.

Start Free Trial View Demos