Redis vs Memcached: Choosing an In-Memory Data Store
Compare Redis vs Memcached for caching. Learn data structures, persistence, performance differences, and when to use each.
Redis vs Memcached
Both Redis and Memcached sit in front of your database and cache frequently accessed data in memory. Both are battle-tested at scale. Developers often use them interchangeably without understanding the differences.
The differences matter. Redis is a data structure server that also happens to support caching. Memcached is a caching engine with a simpler data model. That distinction shapes what you can build with them and how you debug them at 2am.
This is not a “both are good” comparison. I will tell you when each one makes sense.
The Fundamental Difference
Memcached stores strings and nothing but strings. You give it a key, you get back a value. That’s the whole API.
Redis supports strings, lists, hashes, sets, sorted sets, bitmaps, hyperloglogs, geospatial indexes, and streams. It can act as a cache, a session store, a message broker, a rate limiter, and a real-time analytics engine.
# Memcached: everything is a string
memcached.set("user:123", json.dumps(user_data))
user_data = json.loads(memcached.get("user:123"))
# Redis: native data structures
redis.hset("user:123", mapping=user_data)
user_data = redis.hgetall("user:123")
Performance depends on what you’re doing with them.
Data Structures
Strings
Both handle simple string values. Redis just has more ways to manipulate them.
# Memcached
set key "value"
get key
# Redis
set key "value"
get key
# Redis extras
append key " more" # Append to existing
incr count # Atomic increment
decr count # Atomic decrement
setrange key 0 "re" # Overwrite bytes
getrange key 0 3 # Substring retrieval
Lists
Memcached doesn’t have lists. Redis does.
# Redis lists: ordered, push/pop from either end
redis.lpush("queue:jobs", "job1", "job2", "job3")
redis.rpop("queue:jobs") # Returns "job1" (oldest)
redis.lrange("queue:jobs", 0, -1) # Get all
# Common use: recently viewed items, job queues, activity logs
redis.lpush("user:123:views", product_id)
redis.ltrim("user:123:views", 0, 19) # Keep last 20
Sets and Sorted Sets
Memcached has no sets. Redis has both.
# Redis sets: unique, unordered
redis.sadd("user:123:likes", "product1", "product2", "product3")
redis.smembers("user:123:likes")
redis.sismember("user:123:likes", "product1") # O(1) check
# Redis sorted sets: scored sets for leaderboards, priorities
redis.zadd("leaderboard", {"player1": 100, "player2": 200, "player3": 150})
redis.zrevrange("leaderboard", 0, 9, withscores=True) # Top 10
redis.zrank("leaderboard", "player2") # Get rank
Hashes
Memcached has no hashes. Redis does.
# Redis hashes: objects without serialization overhead
redis.hset("user:123", "name", "Alice", "email", "alice@example.com")
redis.hget("user:123", "name") # "Alice"
redis.hgetall("user:123") # All fields
# vs Memcached requiring JSON serialization
memcached.set("user:123", json.dumps({"name": "Alice", "email": "..."}))
Persistence
Memcached: Pure Memory
Memcached is pure memory. It never touches disk. When it restarts, everything is gone.
# Memcached has no persistence options
# Restart = empty cache
This sounds like a drawback, but for pure caching it is fine. Your source of truth is the database anyway.
Redis: Optional Persistence
Redis persists to disk. You can survive restarts without losing data.
# RDB snapshots: point-in-time dumps
save 900 1 # Save if 1 key changed in 900 seconds
save 300 10 # Save if 10 keys changed in 300 seconds
save 60 10000 # Save if 10000 keys changed in 60 seconds
# AOF (Append Only File): every write logged
appendonly yes
appendfsync everysec # fsync every second (balance of speed/safety)
# Or no persistence at all (pure cache mode)
save ""
Redis persistence is configurable. You can turn it off for pure caching or enable it for durability.
Performance
Raw performance depends on your workload. Here’s a general comparison:
| Operation | Memcached | Redis |
|---|---|---|
| GET/SET (simple) | Very fast | Fast |
| MGET/MSET (batch) | Faster | Slower (per-key overhead) |
| INCR (atomic counter) | Fast | Very fast |
| Sets/Lists/Hashes | Not supported | Depends on operation |
| Memory efficiency | Better (simple values) | Depends on data structures |
For simple string caching, Memcached often uses less memory per key. For complex data structures, Redis’s overhead is usually worth it.
Redis uses single-threaded execution (one command at a time per connection, but multiple connections). Memcached is multi-threaded. On a single instance, Redis can saturate network bandwidth. Memcached scales better on multi-core for raw throughput.
# Redis pipelining: batch commands to reduce round trips
pipe = redis.pipeline()
for key in keys:
pipe.get(key)
results = pipe.execute() # One round trip for all
Clustering and Distribution
Memcached
Memcached has no native clustering. You shard across instances manually using consistent hashing.
import hashlib
class ConsistentHash:
def __init__(self, servers):
self.servers = servers
self.ring = {}
self.sorted_keys = []
for server in servers:
for i in range(150):
key = f"{server}:{i}"
hash_key = int(hashlib.md5(key.encode()).hexdigest(), 16)
self.ring[hash_key] = server
self.sorted_keys.append(hash_key)
self.sorted_keys.sort()
def get_server(self, key):
hash_key = int(hashlib.md5(key.encode()).hexdigest(), 16)
for sorted_key in self.sorted_keys:
if hash_key <= sorted_key:
return self.ring[sorted_key]
return self.ring[self.sorted_keys[0]]
It works. You’re just managing the sharding yourself.
Redis
Redis has built-in clustering with automatic sharding.
# Redis Cluster configuration
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 15000
# Automatic sharding, replication, and failover
# Your application sees a single logical database
Redis Cluster partitions keys across nodes automatically. It also supports replication for read scaling and failover.
Eviction Policies
Both support similar eviction policies when memory is full.
# Memcached eviction
# -no-eviction: return error on out-of-memory
# -allkeys-lru: evict least recently used of all keys
# -allkeys-random: evict random
# -volatile-lru: evict LRU of keys with TTL
# -volatile-ttl: evict shortest TTL
# -volatile-random: evict random of keys with TTL
memcached -o expire_counter,merge_threshold,ev=volatile-lru
# Redis maxmemory policies
# allkeys-lru, allkeys-random, allkeys-lfu, allkeys-ttl
# volatile-lru, volatile-lfu, volatile-random, volatile-ttl
# noeviction
maxmemory 100mb
maxmemory-policy allkeys-lru
The policies are nearly identical. Redis adds LFU (Least Frequently Used) which Memcached doesn’t have.
Use Cases
When to Use Memcached
- Simple string caching (HTML fragments, API responses, session data)
- Maximum memory efficiency per key
- Horizontal scaling via consistent hashing is acceptable
- You don’t need complex data structures
- You’re caching database query results that fit naturally in key-value form
Good fits: Varnish is a frontend cache. Memcached is a backend cache. Use it for things that don’t change often and benefit from sub-millisecond access.
When to Use Redis
- You need lists, sets, sorted sets, or hashes
- You want optional persistence
- You need atomic counters (rate limiting, DistributedLOCKS)
- You need pub/sub (real-time features, chat)
- You want built-in clustering
- You’re building leaderboards, job queues, rate limiters, or caching with TTL
- You need Lua scripting for atomic operations
Good fits: Session storage with TTL, rate limiting (INCR + EXPIRE), leaderboards (sorted sets), job queues (lists), pub/sub for real-time, caching with complex data access patterns.
A Practical Decision Framework
Do you need anything beyond simple string key-value?
YES -> Redis
NO -> Does memory efficiency matter more than features?
YES -> Memcached
NO -> Redis (for easier operations and clustering)
If you are not sure, start with Redis. The extra memory usage is negligible for most workloads. If you later find memory is tight and profiling shows Memcached is meaningfully better, switch.
Production Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Redis/Memcached OOM | Cache returns errors; application falls back to database | Monitor used_memory/maxmemory ratio; set alerts at 70% threshold |
| Redis fork for RDB save | Brief blocking during fork; memory doubles during copy-on-write | Schedule RDB saves during low-traffic; use AOF instead for persistence |
| Memcached restart | All data lost immediately (no persistence) | Design for cold cache; implement application-level cache warming |
| Redis replica lag | Reads from replica may return stale data | Monitor replication_backlog_histlen; read from primary for consistency-critical data |
| Connection pool exhaustion | Requests timeout waiting for connection | Size connection pool appropriately; implement request queuing with timeout |
| Single-threaded Redis blocking | Long commands block all other commands | Avoid KEYS, SMEMBERS on large sets; use pipeline/batch operations |
| Memcached multi-thread contention | High CPU under heavy load | Scale horizontally with consistent hashing; consider Redis for complex workloads |
Capacity Estimation: Memory-per-Key and Cluster Slot Planning
Memory per key differs significantly between Redis and Memcached.
Redis memory breakdown per key (string value):
| Component | Size |
|---|---|
| Key pointer | ~56 bytes (SDS allocator) |
| Value storage | Actual value size |
| Redis object overhead | ~16 bytes |
| Dictionary entry (if in hash) | ~32 bytes |
| Total minimum per key | ~72 bytes + value |
Memcached memory breakdown per key (string value):
| Component | Size |
|---|---|
| Key | Key length |
| Value | Actual value size |
| flags byte | 1 byte |
| CAS token (optional) | 8 bytes |
| Expiry time | 4 bytes |
| Overhead per item | ~25 bytes |
| Total minimum per item | ~25 bytes + key + value |
For a cache with 1 million keys, each storing a 200-byte value:
- Redis string: ~72M overhead + 200M data = ~272M total
- Memcached: ~25M overhead + key_space + 200M data = ~240M + key_space
Memcached wins on simple string workloads by 10-20% memory efficiency. Redis pays the overhead for richer data structures.
Redis Cluster slot planning: 16,384 slots divided across N master nodes. For a 6-node cluster (3 masters + 3 replicas), each master owns ~5,461 slots. Slot ownership determines which node stores which keys. The formula: slot = CRC16(key) % 16384. When planning capacity, ensure each master has headroom — if one master owns 5,461 slots and your average key is 1KB with 100K keys per slot, that node needs roughly 5GB. Plan for 2x headroom.
Memcached cluster sizing: No slots — consistent hashing distributes keys. Target 150-200 virtual nodes per physical node for even distribution. With N nodes and V virtual nodes each, the coefficient of variation (CV) of key distribution should stay below 0.3. Formula: CV ≈ 1/√(N × V). For CV < 0.3 with V = 150, you need N ≥ 12 nodes for even distribution. Fewer nodes means higher variance in distribution.
Real-World Case Study: Instagram’s Redis-Memcached Migration
Instagram migrated parts of their caching infrastructure from pure Memcached to Redis in stages. Their challenge: Memcached handled simple key-value caching well, but their social feed, activity streams, and feature like counts needed sorted sets and counters that Memcached could not express efficiently.
Their architecture evolved in phases. Initially everything ran on Memcached — simple, fast, memory-efficient for strings. When they needed atomic counters for like counts and sorted sets for activity feeds, they introduced Redis alongside Memcached. Redis handled the data structures. Memcached continued handling page-level caching.
The operational lesson: Redis requires more expertise to run than Memcached. Redis persistence (RDB snapshots, AOF logs) needs monitoring. Fork times during RDB saves can cause latency spikes on large instances. Instagram’s team had to develop internal tooling to manage Redis at scale before committing to it as a primary store.
The lesson for your infrastructure: start with Memcached for simple caching. Add Redis when you have concrete use cases that need its data structures. Running both is not failure — it is matching the tool to the job.
Interview Questions
Q: Redis uses more memory per key than Memcached for simple strings. How would you optimize a Redis deployment for a memory-constrained environment?
Memcached wins on raw memory efficiency for simple strings because it has minimal per-key overhead. For Redis in memory-constrained environments, use hashes instead of string serialization — HSET user:123 name Alice email alice@example.com stores all fields in one Redis key with shared overhead, versus one key per field or JSON serialization in a string key. Enable maxmemory-policy allkeys-lru and set maxmemory conservatively. Use MEMORY USAGE command to identify large keys. Consider using ziplist encoding for small hashes and lists to compress memory. For pure string caching where memory is critical, Memcached remains the pragmatic choice.
Q: Your Redis instance shows high CPU usage despite moderate request rates. What is likely happening?
Redis is single-threaded, so a single long-running command blocks everything. The slowlog get 10 command reveals which commands are taking >10ms. Common culprits: SORT on large sets, KEYS pattern scans (never use in production), SMEMBERS on large sets, ZRANGEBYSCORE on large sorted sets without LIMIT, or FLUSHDB during peak traffic. For complex operations on large datasets, move the work to the application side — fetch the raw data and process it there. Also check for fork fatigue if using RDB persistence — the fork itself is cheap but if the parent process is CPU-bound, latency spikes occur during fork.
Q: What are the trade-offs between Redis RDB snapshots and AOF persistence for a caching workload?
RDB snapshots are point-in-time dumps — compact, fast to restore, but you lose data since the last snapshot if the instance crashes. AOF logs every write operation — better durability, configurable fsync intervals, but larger files and slower writes. For a pure cache where the database is the source of truth, RDB is usually sufficient — if Redis restarts with an empty cache, the application repopulates from the database. Enable AOF only when you needdurability guarantees for cached data, or when restart time matters more than storage overhead. The appendfsync everysec setting is a good balance — worst case 1 second of data loss but much faster than always.
Q: Memcached is multi-threaded but you observe high CPU and low throughput. What is happening?
Memcached’s multi-threaded architecture uses a global lock on the cache for each operation. If your workload performs very small gets and sets, the lock contention overhead exceeds the parallelism benefit. High CPU with low throughput is the signature of lock contention in Memcached. Workarounds: use connection pooling to multiplex connections (more clients means better parallelism), partition your keys across multiple Memcached instances to reduce per-instance lock contention, or switch to Redis where single-threaded execution eliminates lock contention entirely for most workloads. Profile with stats command — look at lock_ratio or wait_ratio if available in your Memcached version.
Observability Checklist
Metrics to Track
Redis:
# Core metrics via Redis INFO
INFO memory # used_memory, maxmemory, mem_fragmentation_ratio
INFO stats # total_commands_processed, keyspace_hits, keyspace_misses
INFO replication # master_link_status, slave_read_only, replication_lag
INFO clients # connected_clients, blocked_clients
# Calculate hit rate
# hit_rate = keyspace_hits / (keyspace_hits + keyspace_misses)
Memcached:
# Stats command
stats
# Items: curr_items, total_items, evictions
# Memory: bytes, limit_maxbytes
# Hit rate: get_hits, get_misses
# Calculate hit rate
# hit_rate = get_hits / (get_hits + get_misses)
Logs to Capture
import structlog
import time
logger = structlog.get_logger()
class CacheMetrics:
def __init__(self, redis_client):
self.redis = redis_client
self.start_time = time.time()
def track_operation(self, operation, key, hit=True):
logger.info("cache_operation",
operation=operation,
key=key,
cache_hit=hit,
latency_ms=self._measure_latency()
)
def log_memory_pressure(self):
info = self.redis.info('memory')
used = info['used_memory']
maxmem = info['maxmemory']
if maxmem > 0 and used / maxmem > 0.8:
logger.warning("cache_memory_critical",
used_mb=used / 1024 / 1024,
max_mb=maxmem / 1024 / 1024,
fragmentation=info.get('mem_fragmentation_ratio', 1))
Alert Rules
# Redis alerts
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 3m
labels:
severity: critical
annotations:
summary: "Redis memory above 80%"
- alert: RedisReplicationLag
expr: redis_replication_backlog_histlen > 1000000
for: 5m
labels:
severity: warning
annotations:
summary: "Redis replication backlog growing"
# Memcached alerts
- alert: MemcachedMemoryHigh
expr: memcached_bytes / memcached_limit_maxbytes > 0.8
for: 3m
labels:
severity: critical
annotations:
summary: "Memcached memory above 80%"
- alert: MemcachedHighEvictions
expr: rate(memcached_evictions_total[5m]) > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High Memcached eviction rate"
Security Checklist
- Network isolation - Bind to private IPs only; never expose to internet
- Authentication - Redis
requirepass; Memcached SASL authentication - TLS encryption - Enable for connections crossing network boundaries
- Disable dangerous commands - Rename
FLUSHDB,FLUSHALL,CONFIGin Redis - Limit memory - Set
maxmemoryto prevent cache from consuming all RAM - Rate limiting - Prevent client exhaustion attacks
- Key validation - Sanitize key inputs; prevent injection of large keys
- Monitor slow log - Redis
SLOWLOGfor commands taking >10ms
# Redis secure configuration
bind 127.0.0.1 -::1
requirepass your-secure-password
tls-port 6379
rename-command FLUSHDB ""
rename-command FLUSHALL ""
rename-command CONFIG ""
maxmemory 1gb
maxmemory-policy allkeys-lru
# Memcached secure configuration
# -S (SASL authentication)
# -l 127.0.0.1 (bind to localhost)
memcached -S -l 127.0.0.1 -u memcached -c 1024
Common Pitfalls / Anti-Patterns
1. Using KEYS Command in Production
The KEYS command scans all keys and blocks Redis. Never use it in production.
# BAD: KEYS blocks Redis for seconds
all_keys = redis.keys("user:*")
# GOOD: Use SCAN for production
cursor = 0
while True:
cursor, keys = redis.scan(cursor, match="user:*", count=100)
process(keys)
if cursor == 0:
break
2. Storing Large Values Without Compression
Large values consume memory disproportionately and slow down operations.
# BAD: Storing large uncompressed data
redis.set("page:123", large_html_content) # 500KB+ per page
# GOOD: Compress large values
import zlib
compressed = zlib.compress(large_html_content.encode())
redis.setex("page:123", 3600, compressed)
3. Not Using Connection Pooling
Each operation creating a new connection adds overhead.
# BAD: New connection each time
def get_user(user_id):
r = redis.Redis(host='localhost', port=6379) # Connection every call
return r.get(f"user:{user_id}")
# GOOD: Reuse connection
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=50)
def get_user(user_id):
r = redis.Redis(connection_pool=pool)
return r.get(f"user:{user_id}")
4. Ignoring Memcached Persistence Limitations
Memcached has no persistence. Data is lost on restart.
# BAD: Assuming Memcached persists data
memcached.set("session:123", session_data)
# ... server restarts ...
session = memcached.get("session:123") # None - data gone
# GOOD: Design for cold start
session = memcached.get("session:123")
if not session:
session = load_from_database() # Always have fallback
memcached.set("session:123", session, time=3600)
5. Using Redis Single Instance for Everything
Redis single-threaded nature means CPU-bound operations block everything.
# BAD: CPU-heavy operation in Redis
# This blocks all other commands
redis.sort("large-set") # O(N log N) - blocks Redis
# GOOD: Move CPU work to application
data = redis.lrange("large-list", 0, -1)
sorted_data = sorted(data) # Application handles sorting
Quick Recap
Key Bullets
- Redis offers data structures (lists, sets, hashes) that Memcached cannot match
- Memcached is more memory-efficient for simple string caching
- Redis persistence (RDB/AOF) survives restarts; Memcached does not
- Redis Cluster provides automatic sharding; Memcached requires client-side sharding
- Both support LRU/LFU eviction but Redis LFU is more sophisticated
- Redis single-threaded is a feature (no race conditions) but means CPU-heavy ops block
Copy/Paste Checklist
# Redis quick setup
redis-server --requirepass your-password --maxmemory 1gb --maxmemory-policy allkeys-lru
# Memcached quick setup
memcached -d -m 1024 -l 127.0.0.1 -p 11211 -S
# Redis monitoring one-liner
redis-cli INFO stats | grep -E "keyspace|connected_clients|used_memory"
# Memcached monitoring one-liner
echo "stats" | nc localhost 11211 | grep -E "curr_items|evictions|bytes"
# Redis connection pool (Python)
pool = redis.ConnectionPool(host='localhost', port=6379, max_connections=50)
r = redis.Redis(connection_pool=pool)
# Memcached connection pooling (Python)
import pymemcache
client = pymemcache.client.base.Client('localhost:11211')
# Decision checklist:
# [ ] Need data structures beyond strings? -> Redis
# [ ] Pure string caching, memory-constrained? -> Memcached
# [ ] Need persistence across restarts? -> Redis
# [ ] Need built-in clustering? -> Redis
# [ ] Simple key-value, maximum efficiency? -> Memcached
See Also
- Cache Eviction Policies — How LRU, LFU, and other policies work in Redis and Memcached
- Caching Strategies — How to use these tools effectively
- Distributed Caching — Scaling Redis and Memcached across multiple nodes
Conclusion
Memcached is simpler and more memory-efficient for pure string caching. Redis is more capable. For basic caching, they are comparable. But Redis’s data structures unlock patterns that would be painful or impossible with Memcached.
I default to Redis for new projects. The operational simplicity of having one system for caching, sessions, pub/sub, and rate limiting usually beats the memory efficiency gains of Memcached.
That said, if you are caching primarily string data and memory is tight, Memcached still earns its place.
Category
Related Posts
Cache Eviction Policies: LRU, LFU, FIFO, and More Explained
Learn LRU, LFU, FIFO, and TTL eviction policies. Understand trade-offs with real-world performance implications for caching.
Caching Strategies: Cache-Aside, Write-Through, and More
Master five caching strategies for production systems. Learn cache-aside vs write-through, avoid cache stampede, and scale with these patterns.
Cache Stampede Prevention: Protecting Your Cache
Learn how single-flight, request coalescing, and probabilistic early expiration prevent cache stampedes that can overwhelm your database.