Why Memcached Wins for Pure Caching: An Operational Perspective
The Redis Problem Nobody Talks About Until It Bites
When the conversation shifts to "we need a cache," most teams reach for Redis. It's a solid tool, well-featured, and everyone knows it. You hand off the connection string and move on.
Then, months in, something breaks. Maybe you upgrade Redis, migrate it to a different node, or a disk fails. You discover the hard way that your app is treating Redis as a database, not a cache. People have been using cache.set() to store persistent state that the system depends on.
Now Redis is embedded in your architecture, tied to application logic, and you're monitoring it like a pet that requires constant feeding.
⚠️ The root issue isn't Redis—it's that teams bring it in as a cache but never establish operational boundaries. Without explicit agreement that the cache is volatile and can disappear, developers treat it like a database. Your monitoring assumes it's stateless. Your runbooks assume it's expendable. Then it isn't, and you're stuck maintaining something that was never meant to be permanent.
The Memcached Advantage
Memcached is simpler. Intentionally. Its homepage says it plainly: a distributed memory caching system, meant to speed up web apps by reducing database load.
That simplicity is the entire point. Here's why memcached wins operationally:
Downtime Is Trivial
When memcached goes down, client libraries handle it gracefully. A cache miss just returns the default or none. Your app keeps running. The database gets a hit, but it's designed to handle that.
With Redis, you have to decide: do you fail the request, return stale data, or retry? That decision belongs in your application, not forced by the cache layer.
Clustering Is Client-Side
Memcached has no built-in clustering. To scale it, you configure the client library with multiple addresses. The client hashes the key, picks a memcached instance, and runs the operation.
If an instance goes down, the client detects the failure, removes it from the hash ring, and distributes the load to the remaining nodes. After a timeout, it automatically reconnects and tries the dead node again.
This is operationally clean. You don't manage consensus, failover, or cluster state. The client layer owns it, and you provision instances as stateless workloads on Kubernetes, VMs, or bare metal—wherever.
Statelessness Is Guaranteed
Memcached doesn't persist. Period. That forces architectural honesty: if you need persistence, you need a database. If you need the data to survive a restart, it's not cache.
This constraint prevents the exact problem that Redis creates: the creeping assumption that the cache layer is reliable.
The Real Trade-Off
Redis has transactions, pub/sub, Lua scripting, persistence options, and data structure richness. Memcached is a key-value store. If you need those features, use Redis. But if you're reaching for it as a cache, you're buying complexity you don't need.
⚠️ Most "database too slow" problems are actually "query too slow" or "missing indices." Before adding a cache layer, fix the query. A well-indexed database and proper query optimization will outpace any caching complexity.
When to Use Memcached
Use memcached when:
- You're caching database query results or computed values
- The cache should be transparent to failure (if it's gone, compute again)
- You want to scale horizontally without managing cluster state
- You need operational simplicity over feature richness
- You run dozens of instances on minimal resources (memcached instances with 64 MB cache sizes run with near-zero overhead)
Use Redis when:
- You need persistence (write-ahead logs, snapshotting)
- You require transactions or atomic operations across multiple keys
- You're using data structures beyond strings (sorted sets, streams, etc.)
- You need pub/sub or real-time messaging
- You're using it as a session store where loss = user friction (though memcached works here too, it's just less forgiving)
Practical Deployment
Memcached on Kubernetes
Memcached stateless deployment with automatic client-side failover:
apiVersion: apps/v1
kind: Deployment
metadata:
name: memcached
spec:
replicas: 3
selector:
matchLabels:
app: memcached
template:
metadata:
labels:
app: memcached
spec:
containers:
- name: memcached
image: memcached:1.6-alpine
ports:
- containerPort: 11211
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: memcached
spec:
clusterIP: None
selector:
app: memcached
ports:
- port: 11211
Client code picks up all three endpoints via DNS and handles failures transparently:
import pylibmc
mc = pylibmc.Client(['memcached-0.memcached', 'memcached-1.memcached', 'memcached-2.memcached'])
value = mc.get('key')
if value is None:
value = expensive_compute()
mc.set('key', value, time=300)
Self-Hosted
On a VPS or dedicated box, memcached is trivial:
docker run -d --name memcached -p 127.0.0.1:11211:11211 memcached:1.6-alpine memcached -m 512
That's it. 512 MB cache, no persistence, no cluster coordination. If it dies, restart it. If you need more capacity, spawn another container on a different port and add the address to your client config.
⚠️ Bind memcached only to localhost or a private network—memcached has no authentication and allows arbitrary get/set/delete operations. Never expose it to 0.0.0.0 in production.
The Operational Reality
Redis as a cache creates operational debt because its features blur the line between caching and persistence. Memcached enforces that line architecturally.
If your developers are using the cache as a database, that's an application design problem. Fix it. Memcached makes that problem visible. Redis lets you hide it until it causes an outage.
For pure caching workloads—session storage, query result caching, computed value caching, rate limiting state—memcached's simplicity wins. You provision it like any other stateless service, monitor it lightly, and accept that cache misses are part of the design.
That's the kind of operational clarity that scales without creating toil.