Redis vs. Memcached
When Redis is the right cache and when Memcached still wins: data structure richness, persistence, replication, clustering, memory efficiency, and the operational differences that matter at scale.
TL;DR
| Dimension | Choose Redis | Choose Memcached |
|---|---|---|
| Data structures | Need hashes, sorted sets, lists, streams, or any non-string type | Only caching simple string/blob values |
| Persistence | Need data to survive restarts (sessions, counters, rate limits) | Pure cache where cold start is acceptable |
| HA / Replication | Need automatic failover and read replicas | Node failure = cache miss, and that's fine |
| Memory efficiency | Key count under ~50M, or willing to trade memory for features | 100M+ small string keys where per-key overhead matters |
| Threading | Single-thread ceiling not hit (vast majority of deployments) | Need to saturate 16+ cores with simple GET/SET on one box |
Default answer: Redis. Memcached wins in a narrow band of use cases (pure string caches at massive key counts). For everything else, Redis does what Memcached does plus a lot more.
The Framing
Every few months I see the same Slack thread: "Should we use Redis or Memcached?" The answer is almost always Redis, but the conversation takes forever because people confuse two different questions.
Question one: which is a better cache? Here, Memcached is genuinely competitive. It's simpler, uses less memory per key for string values, and its multi-threaded architecture means a single Memcached node can saturate all available cores for simple GET/SET workloads.
Question two: which is a better data store for things that happen to live in memory? This isn't even close. Redis supports data structures, persistence, replication, pub/sub, Lua scripting, and streams. Memcached supports strings. The moment you need a sorted leaderboard, a rate limiter, session storage with failover, or a pub/sub channel, Memcached is out.
The real decision comes down to this: are you building a pure throwaway cache for string blobs, or do you need any feature beyond GET/SET? If the first, evaluate Memcached. If the second, stop evaluating and use Redis.
How Each Works
Redis: Single-Threaded Event Loop + Rich Data Model
Redis runs a single-threaded event loop for command execution. One thread processes all client commands sequentially, which eliminates locking and makes every operation atomic by default. Starting with Redis 6, I/O threads handle network reads and writes in parallel, but the command execution thread remains single.
The data model is where Redis separates itself. Every value has a type, and Redis provides type-specific operations:
# Strings (same capability as Memcached)
SET session:abc123 "{user_id: 456}" EX 3600
# Hashes: update one field without fetching the whole object
HSET user:456 login_count 0
HINCRBY user:456 login_count 1
# Sorted Sets: leaderboard with automatic ranking
ZADD leaderboard 9842 "user:456"
ZREVRANK leaderboard "user:456" # Returns rank, no app-side sort
# Lists: bounded notification queue
LPUSH notifications:456 "You have a new message"
LTRIM notifications:456 0 99 # Keep last 100
# Streams: append-only log with consumer groups
XADD events * type "purchase" amount "49.99"
Persistence is optional. RDB snapshots create point-in-time dumps; AOF (Append-Only File) logs every write for durability. You can run both. For pure caching, disable persistence entirely and Redis behaves like Memcached with more features.
Replication uses async leader-follower replication. Redis Sentinel monitors the leader and promotes a follower on failure. Redis Cluster shards data across multiple nodes with per-shard replication.
Memcached: Multi-Threaded Slab Allocator + Strings Only
Memcached runs multiple worker threads, each handling connections and executing commands concurrently. On a 16-core machine, Memcached uses all 16 cores for command processing. This is its architectural advantage for raw throughput on simple operations.
The data model is intentionally minimal: keys map to byte arrays. No types, no server-side operations beyond GET, SET, DELETE, and atomic increment/decrement.
# Set a value with 1-hour TTL
set session:abc123 0 3600 42
{serialized_json_bytes}
# Get it back
get session:abc123
# Atomic increment (only works if value is a numeric string)
incr page_views:home 1
Memory management uses a slab allocator. Memory is divided into slabs of fixed sizes (64B, 128B, 256B, up to 1MB). Each value goes into the smallest slab class that fits it. This avoids external fragmentation but wastes space when values don't fill their slab. LRU eviction is per-slab class, not global.
There is no persistence, no replication, and no clustering built in. Clients implement consistent hashing to distribute keys across multiple Memcached nodes. If a node dies, those keys are gone and requests fall through to the database.
Redis Data Structures in Practice
The syntax examples above barely scratch the surface. Here's what these data structures look like in production, where Memcached would require complex, race-prone application code to achieve the same result.
Sorted set leaderboard: A mobile game needs a global ranking across 10 million players. Redis makes the ranking automatic.
# Player finishes a round with a new high score
redis.zadd("leaderboard:season_4", {"player:8829": 15230})
# Get this player's rank (0-indexed from the top)
rank = redis.zrevrank("leaderboard:season_4", "player:8829") # O(log N)
# Top 10 players with scores, one command
top_10 = redis.zrevrange("leaderboard:season_4", 0, 9, withscores=True)
# Players ranked 50-60 (pagination for "players near me")
nearby = redis.zrevrange("leaderboard:season_4", 50, 60, withscores=True)
With Memcached, you'd serialize the leaderboard as JSON, fetch it on every update, sort it in application code, and write it back. Two concurrent updates overwrite each other. Even with CAS tokens, the retry storm at high write rates kills performance.
Hash for user sessions: A session store where you frequently update individual fields without fetching the whole object.
# Create session with multiple fields
redis.hset("session:abc123", mapping={
"user_id": "456", "role": "admin",
"cart_items": "3", "last_active": "1712345678"
})
# Update just last_active (no read-modify-write cycle)
redis.hset("session:abc123", "last_active", str(int(time.time())))
# Increment cart count atomically, no race condition
redis.hincrby("session:abc123", "cart_items", 1)
# Read the full session in one round trip
session = redis.hgetall("session:abc123")
Pub/Sub for cache invalidation fan-out: When a product is updated, every app server needs to purge its local cache instantly.
# Publisher (write path, runs once per product update)
def update_product(product_id, data):
db.update("products", product_id, data)
redis.publish("cache:invalidate", f"product:{product_id}")
# Subscriber (runs on every app server)
def listen_for_invalidations():
pubsub = redis.pubsub()
pubsub.subscribe("cache:invalidate")
for message in pubsub.listen():
local_cache.delete(message["data"])
This fan-out pattern is impossible in Memcached. You'd either poll for changes (adding latency and load) or accept stale data until TTL expiry. I've seen teams build custom invalidation systems on top of Memcached using message queues, which is just reimplementing pub/sub with extra infrastructure.
The Threading Model
This is the single biggest architectural difference, and it matters far less often than people think.
Redis: Single-Threaded Event Loop
Redis processes all commands on a single thread using an event loop (epoll on Linux, kqueue on macOS). Every command runs to completion before the next one starts. This eliminates locking entirely and makes every operation atomic by default.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.