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.

published: reading time: 17 min read

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:

OperationMemcachedRedis
GET/SET (simple)Very fastFast
MGET/MSET (batch)FasterSlower (per-key overhead)
INCR (atomic counter)FastVery fast
Sets/Lists/HashesNot supportedDepends on operation
Memory efficiencyBetter (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

FailureImpactMitigation
Redis/Memcached OOMCache returns errors; application falls back to databaseMonitor used_memory/maxmemory ratio; set alerts at 70% threshold
Redis fork for RDB saveBrief blocking during fork; memory doubles during copy-on-writeSchedule RDB saves during low-traffic; use AOF instead for persistence
Memcached restartAll data lost immediately (no persistence)Design for cold cache; implement application-level cache warming
Redis replica lagReads from replica may return stale dataMonitor replication_backlog_histlen; read from primary for consistency-critical data
Connection pool exhaustionRequests timeout waiting for connectionSize connection pool appropriately; implement request queuing with timeout
Single-threaded Redis blockingLong commands block all other commandsAvoid KEYS, SMEMBERS on large sets; use pipeline/batch operations
Memcached multi-thread contentionHigh CPU under heavy loadScale 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):

ComponentSize
Key pointer~56 bytes (SDS allocator)
Value storageActual 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):

ComponentSize
KeyKey length
ValueActual value size
flags byte1 byte
CAS token (optional)8 bytes
Expiry time4 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, CONFIG in Redis
  • Limit memory - Set maxmemory to 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 SLOWLOG for 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


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 #algorithms #system-design

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.

#caching #system-design #performance

Cache Stampede Prevention: Protecting Your Cache

Learn how single-flight, request coalescing, and probabilistic early expiration prevent cache stampedes that can overwhelm your database.

#cache #cache-stampede #performance