When to Use Redis vs Memcached in 2026 - NextGenBeing When to Use Redis vs Memcached in 2026 - NextGenBeing
Back to discoveries

When to Use Redis vs Memcached in 2026

A 64 GB cache node holding 41 GB of live data should not be evicting anything. Yet stats on a memcached instance in exactly that state can report evictions climbing by 12,000 per second while…

Web Development 14 min read
Bekzod Erkinov

Bekzod Erkinov

Sep 25, 2026 • 0 views
When to Use Redis vs Memcached in 2026
Photo by Tyler on Unsplash
Size:
Height:
📖 14 min read 📝 4,499 words 👁 Focus mode: ✨ Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished
Table of contents · 11 sections

When to Use Redis vs Memcached in 2026

A 64 GB cache node holding 41 GB of live data should not be evicting anything. Yet stats on a memcached instance in exactly that state can report evictions climbing by 12,000 per second while bytes sits comfortably under limit_maxbytes. Run stats slabs and the reason appears: slab class 7 (chunk size 1184 bytes) owns 300 pages and is thrashing, while class 3 (chunk size 192 bytes) owns 28,000 pages that were allocated three weeks ago for an object shape the application no longer writes. Memory is not fungible in memcached. Pages belong to a slab class permanently unless you turn on automove.

That single behaviour — and the fact that Redis does not have it, because it hands allocation to jemalloc — is a better starting point for choosing between the two systems than any benchmark chart. Both are mature, both are fast enough that the network is usually the bottleneck, and both will serve a million operations per second on hardware you can rent by the hour. The choice in 2026 is about failure modes, memory accounting, licensing, and how much of your application's logic you want to push into the cache tier.

What actually changed by 2026

Redis's licence moved off BSD in March 2024 with Redis 7.4, to the dual RSALv2/SSPLv1 arrangement. The Linux Foundation forked the last BSD commit into Valkey, which shipped 8.0 in September 2024 with asynchronous I/O threading and a rebuilt dictionary layout. Redis then added AGPLv3 as a third option in Redis 8.0 (May 2025), and folded the formerly separate modules — JSON, Search, TimeSeries, Bloom/Cuckoo filters, and the newer Vector Sets type — into the core distribution.

The practical consequence: "Redis" is now at least three things. Redis Open Source under AGPLv3, Valkey under BSD-3, and a pile of managed services (ElastiCache, MemoryDB, Memorystore, Azure Cache) that may be running either. If your legal team has a position on AGPL, Valkey is the drop-in: same RESP protocol, same redis-cli, same client libraries, different INFO server output.

Memcached, meanwhile, did what memcached always does: nothing dramatic. The 1.6.x line has been stable for years. The interesting capabilities landed quietly — extstore (SSD-backed item storage), the meta commands protocol, restartable cache via memory-mapped files, and a built-in Lua-scriptable proxy. Most teams evaluating memcached in 2026 are evaluating a 2018 mental model of it, which is a mistake worth correcting before you decide.

The threading model, and where it actually bites

Memcached is multithreaded by design. -t 4 gives you four worker threads, each with its own event loop; connections are assigned to a thread at accept time. Access to the item hash table and the LRU is protected by a stripe of item locks, so two GETs for different keys genuinely execute in parallel across cores.

Redis executes commands on a single thread. io-threads (4 or 8 is typical) parallelises socket reads, protocol parsing, and reply writes, but command execution itself remains serialised. Valkey 8 pushed further, moving more of the read path off the main thread with an asynchronous design.

# Memcached: four worker threads, 8 GB, 4096 max connections,
# and a larger-than-default hash table to avoid rehash pauses
memcached -m 8192 -t 4 -c 4096 -o hashpower=22 -l 0.0.0.0 -p 11211
# redis.conf — I/O threading, effective from Redis 6 onward
io-threads 8
io-threads-do-reads yes
maxmemory 8gb
maxmemory-policy allkeys-lru

Where this matters is not peak throughput; it is tail latency in the presence of one slow command. A single KEYS *, an HGETALL over a 400,000-field hash, or a Lua script that loops 50,000 times blocks every other client on that Redis instance. The p99 for unrelated GETs goes from 0.4 ms to whatever that command takes. Memcached has no equivalent foot-gun, because it has no command that can take 200 ms.

Find these before they find you:

redis-cli CONFIG SET slowlog-log-slower-than 5000   # microseconds
redis-cli SLOWLOG GET 10
redis-cli --latency-history -i 5                    # rolling min/avg/max
redis-cli --bigkeys                                 # sampling scan for outliers

If your workload is purely GET/SET of opaque blobs at very high concurrency on a many-core box, memcached's threading gives it a real, measurable edge — often 1.5x to 2x the single-instance throughput at the same p99. If your workload is mixed and you would run several Redis processes per host anyway (which is what Redis Cluster on one machine amounts to), the gap mostly closes.

Memory accounting: slabs versus jemalloc

This is the deepest architectural difference, and the one that produces the most surprising production incidents.

Memcached pre-allocates 1 MB pages and carves each into fixed-size chunks. Chunk sizes follow a growth factor, default 1.25, starting near 96 bytes. A 700-byte item lands in a class with 768-byte chunks and wastes 68 bytes. Worse, once a page belongs to a class it stays there — the calcification problem from the opening.

# Inspect the damage
echo -e "stats slabs\r" | nc 127.0.0.1 11211 | head -40
echo -e "stats items\r" | nc 127.0.0.1 11211 | grep evicted_unfetched

evicted_unfetched is the number worth alerting on: items evicted that were never read even once. A high value means you are paying to store things you never serve.

The fix has existed for years but is not always enabled in older packaging:

memcached -m 8192 -o slab_reassign,slab_automove=2,modern

slab_automove=2 is the aggressive mode — it moves pages between classes as soon as it detects one class evicting while another sits idle. The -f flag changes the growth factor; -f 1.08 gives finer-grained classes and less per-item waste at the cost of more classes to manage. Sizing this well requires knowing your object size distribution, which you should measure rather than guess.

Redis allocates per-object through jemalloc. There is no calcification, but there is fragmentation, and Redis reports it honestly:

redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio|maxmemory_human'

A mem_fragmentation_ratio of 1.03 to 1.10 is healthy. Above 1.5 on a long-running instance with churn, enable active defragmentation:

activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 5
active-defrag-cycle-max 75

Per-key overhead differs too. A memcached item carries roughly a 48–56 byte header plus the key plus a length/flags suffix. A Redis string key costs roughly 50–90 bytes of bookkeeping (dict entry, object header, SDS header, plus jemalloc bin rounding) before your value. For ten million tiny keys that is around 700 MB of pure overhead — which is exactly why Redis ships compact encodings for small aggregates. The Redis memory optimization guide documents the thresholds:

hash-max-listpack-entries 128
hash-max-listpack-value 64
zset-max-listpack-entries 128
set-max-intset-entries 512

Storing ten million counters as ten million STRING keys versus as 78,125 hashes of 128 fields each is roughly a 5x to 8x memory difference, because a listpack-encoded hash stores fields as one contiguous byte array with no per-field object headers. Confirm the encoding is what you think it is:

redis-cli HSET bucket:1042 user:8830 17
redis-cli OBJECT ENCODING bucket:1042    # -> "listpack", not "hashtable"

Once a single field exceeds 64 bytes or the hash exceeds 128 entries, Redis converts to a real hash table and never converts back, even if you delete the offending fields afterwards.

Eviction policy: approximate LRU versus segmented LRU

Redis defaults to maxmemory-policy noeviction, which is the correct default for a database and the wrong one for a cache. With noeviction and a full instance, writes fail:

(error) OOM command not allowed when used memory > 'maxmemory'.

That error, appearing in application logs at 3 a.m., is the single most common Redis-as-cache misconfiguration. Set the policy explicitly:

redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli CONFIG SET maxmemory-samples 10
redis-cli CONFIG REWRITE

Redis's LRU is approximate: it samples maxmemory-samples keys (default 5) and evicts the best candidate from that sample. Raising it to 10 gets close to true LRU at modest CPU cost. allkeys-lfu uses a probabilistic counter with logarithmic increment and time-based decay, and it is usually the better choice for a read cache with a hot subset — it resists the classic failure where one large batch job evicts your entire working set. The Redis eviction documentation covers the counter tuning parameters (lfu-log-factor, lfu-decay-time).

Memcached uses a segmented LRU with HOT, WARM, and COLD queues plus a background crawler that reclaims expired items rather than waiting for something to touch them. It is genuinely good, needs no tuning in most cases, and is scoped per slab class — which is both its strength (no global lock) and the source of calcification.

Data structures, or: is this a cache or a data layer?

Memcached stores bytes under a key. That is the entire data model, plus atomic incr/decr, add, append, prepend, and compare-and-swap. If that is what you need, the constraint is a feature — there is nothing to misuse.

Redis gives you sorted sets, streams, bitmaps, HyperLogLog, geospatial indexes, and server-side Lua and Functions. The question is whether your problem is actually a cache problem.

A sliding-window rate limiter is the canonical case where Redis wins outright:

-- rate_limit.lua: sliding window, atomic, one round trip
-- KEYS[1] = bucket key, ARGV[1] = now_ms, ARGV[2] = window_ms, ARGV[3] = limit
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[1]) - tonumber(ARGV[2]))
local used = redis.call('ZCARD', KEYS[1])
if used >= tonumber(ARGV[3]) then
  return {0, used}
end
redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1] .. ':' .. math.random(1000000000))
redis.call('PEXPIRE', KEYS[1], tonumber(ARGV[2]))
return {1, used + 1}
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$sha = $redis->script('load', file_get_contents('rate_limit.lua'));

[$allowed, $used] = $redis->evalSha(
    $sha,
    ['ratelimit:api:' . $userId, (int) (microtime(true) * 1000), 60000, 100],
    1
);

if (!$allowed) {
    http_response_code(429);
    header('Retry-After: 60');
    exit;
}

Implementing that on memcached means a read-modify-write loop with CAS retries, and it is racy under contention. Conversely, caching a rendered HTML fragment or a serialised ORM result is identical work in both systems, and the one that costs less per gigabyte wins.

A useful rule: if you catch yourself writing WATCH/MULTI/EXEC or a Lua script, you have left cache territory and entered data-structure-server territory. That is fine — just be honest that you now run a stateful component whose loss is not merely a latency event.

Stampede protection is a solved problem on both sides

Memcached's meta protocol handles this natively. mg with the N flag auto-vivifies a placeholder on miss, and R hands exactly one client a recache "win" token when the remaining TTL drops below a threshold:

# Meta-get: return value + flags, vivify for 30s on miss,
# award a win token if fewer than 10s of TTL remain
printf 'mn\r\n' | nc 127.0.0.1 11211
printf 'mg product:9917 v f t N30 R10\r\n' | nc 127.0.0.1 11211

The client that receives the W flag rebuilds the value; everyone else gets the stale value marked with X and serves it. The memcached meta commands reference documents the full flag set. This is stale-while-revalidate implemented inside the cache server, and it is genuinely excellent.

The Redis equivalent is a mutex key plus application logic:

import time, uuid, redis

r = redis.Redis(decode_responses=True)

RELEASE = """
if redis.call('get', KEYS[1]) == ARGV[1] then
  return redis.call('del', KEYS[1])
end
return 0
"""

def get_with_lock(key, builder, ttl=300, lock_ttl=10):
    val = r.get(key)
    if val is not None:
        return val

    token = str(uuid.uuid4())
    if r.set(f"lock:{key}", token, nx=True, ex=lock_ttl):
        try:
            val = builder()
            r.set(key, val, ex=ttl)
            return val
        finally:
            r.eval(RELEASE, 1, f"lock:{key}", token)

    # Someone else is rebuilding; back off briefly, then read through.
    time.sleep(0.05)
    return r.get(key) or builder()

More code, more edge cases, same outcome.

Durability, restarts, and what a cold cache costs

Memcached has no persistence. Restart the process and the data is gone — except that it supports a restartable cache mode, which memory-maps the item store to a file and reattaches on a clean restart:

memcached -m 8192 -e /mnt/pmem/memcached_state -o modern
# Requires a clean shutdown. A SIGKILL, an OOM kill, or a binary
# version change invalidates the file and you start cold.

That covers planned restarts for config changes and upgrades. It does not cover crashes, and it does not survive hardware loss.

Redis offers RDB snapshots, AOF with appendfsync everysec, or both. For a cache, the useful question is not "do I need durability" but "what happens during the 90 seconds after a cold start?" If your origin is a Postgres primary that handles 3,000 QPS with the cache warm and would need 60,000 QPS with it cold, the cache is load-bearing infrastructure and you need either persistence or a replica to fail over to.

# Cache-oriented Redis persistence: RDB only, infrequent, no AOF
save 900 1
save 300 100
appendonly no
stop-writes-on-bgsave-error no
rdbcompression yes

stop-writes-on-bgsave-error no matters more than it looks. The default yes makes Redis reject all writes if a background save fails, which turns a disk-full condition on a cache into a full application outage.

Note also that a loading instance answers every command with:

LOADING Redis is loading the dataset in memory

Your client library must treat that as retryable, not as a hard failure. Many do not, by default.

Scaling out

Memcached has no server-side clustering. Clients shard with consistent hashing (ketama), and the standard production pattern adds a routing layer — mcrouter, or memcached's own built-in proxy:

-- proxy.lua for memcached's built-in proxy (-o proxy_config=...)
pools{
  main = { backends = {
    "10.0.1.11:11211", "10.0.1.12:11211", "10.0.1.13:11211",
  }},
}

routes{
  map = {
    session = route_allfastest{ children = { "main" } },
  },
  default = route_ketama{ children = { "main" } },
}

Adding a node to a ketama ring moves roughly 1/N of the keys, which is a bounded, self-healing miss spike. No rebalancing job, no slot migration, no cluster state to diverge.

Redis Cluster hashes keys into 16,384 slots via CRC16 and owns the topology server-side. Clients follow redirects:

(error) MOVED 3999 10.0.1.12:6379

and discover the map through CLUSTER SHARDS. The cost is a genuine constraint on multi-key operations:

(error) CROSSSLOT Keys in request don't hash to the same slot

Hash tags fix it when the keys are genuinely related — user:{8830}:profile and user:{8830}:prefs both hash on 8830 and land on the same shard — but that is a design decision you must make before you write the key naming scheme, not after.

# docker-compose.yml — a 3-shard Redis Cluster for local verification
services:
  redis-1: &node
    image: redis:8-alpine
    command: >
      redis-server --cluster-enabled yes
      --cluster-config-file nodes.conf
      --cluster-node-timeout 5000
      --maxmemory 512mb --maxmemory-policy allkeys-lfu
      --appendonly no
    ports: ["7001:6379"]
  redis-2:
    <<: *node
    ports: ["7002:6379"]
  redis-3:
    <<: *node
    ports: ["7003:6379"]
docker compose up -d
docker compose exec redis-1 redis-cli --cluster create \
  redis-1:6379 redis-2:6379 redis-3:6379 --cluster-yes
docker compose exec redis-1 redis-cli --cluster check redis-1:6379

Benchmark your own key distribution

Generic benchmark numbers are close to useless, because throughput is dominated by value size, pipeline depth, and key locality. memtier_benchmark speaks both protocols, so you can compare like for like on your own hardware:

# Redis: 80/20 read/write, 2 KB values, skewed key access
memtier_benchmark -s 127.0.0.1 -p 6379 --protocol=redis \
  -t 4 -c 50 --pipeline=8 --ratio=1:4 \
  --data-size=2048 --key-pattern=G:G --key-stddev=10000 \
  --test-time=120 --hide-histogram

# Memcached, same shape
memtier_benchmark -s 127.0.0.1 -p 11211 --protocol=memcache_binary \
  -t 4 -c 50 --pipeline=8 --ratio=1:4 \
  --data-size=2048 --key-pattern=G:G --key-stddev=10000 \
  --test-time=120 --hide-histogram

Run both against the same instance type and read the p99 column, not the ops/sec column. Then vary --data-size across your actual distribution. In comparisons shaped like this, the two typically land within 20% of each other at small values, memcached pulls ahead as thread count rises, and Redis pulls ahead when pipelining is deep because its protocol parsing is cheaper per command.

extstore changes the cost equation

Memcached's extstore keeps keys and metadata in RAM while spilling values to SSD:

memcached -m 4096 -o ext_path=/var/lib/memcached/cache:512G \
  -o ext_wbuf_size=32,ext_item_age=60,ext_threads=4 \
  -o slab_automove=2,modern

On NVMe this yields tens of terabytes of cache per node at single-digit-millisecond p99 for flash hits, at a fraction of the cost of equivalent DRAM. Redis Open Source has no in-core equivalent; tiering exists only in Redis Enterprise and a few managed tiers. For very large, cold-tolerant caches — rendered pages, image derivatives, search result sets — extstore is often the deciding factor, and it is the most underused feature in this whole comparison. The extstore documentation covers the write-amplification trade-offs and which workloads it suits.

Choosing: a short decision procedure

Pick memcached when the data model is opaque blobs keyed by string; when you want multithreaded scaling on one large box; when you need a very large cache backed by SSD via extstore; when you value a system with no configuration surface that can turn a cache incident into a data incident; or when proxy-level routing (route_allfastest, failover pools) already solves your topology.

Pick Redis or Valkey when you need any data structure beyond a blob; when you need atomic multi-step operations; when you want pub/sub, streams, or consumer groups alongside the cache; when you need server-side replication and automatic failover without a separate proxy; when your cache is load-bearing and must survive restarts; or when running one system instead of three is worth real money in operational time.

Pick both when the sizes justify it: memcached for the large fragment and page cache, Redis for sessions, locks, queues, and counters. Two systems is more operational surface, but they are both simple enough that the total is often less work than bending one to do the other's job.

Common pitfalls

Leaving maxmemory unset in Redis. Without it, Redis grows until the kernel OOM killer intervenes. Set maxmemory to roughly 60–70% of instance RAM — BGSAVE forks, and copy-on-write during heavy writes can nearly double the resident set.

Leaving maxmemory-policy at noeviction. See the OOM error above. This is a cache; it must be allowed to forget.

Exceeding memcached's 1 MB item limit. You get SERVER_ERROR object too large for cache. Raising it with -I 8m also changes the slab math and increases waste; chunking the value in the client is usually the better fix.

Assuming TTL is the only expiry. Both systems evict under pressure. A key with a 24-hour TTL can vanish in 30 seconds. Never treat a cache entry as authoritative for anything — sessions included — unless you have replication and persistence behind it.

Using KEYS in production. It is O(N) and blocks the single thread. Use SCAN with a COUNT hint and honour the returned cursor; expect duplicates and handle them.

Ignoring CROSSSLOT until migration day. Design key names with hash tags before you deploy to a single node, or plan for a rewrite when you shard.

Clock skew and memcached TTLs. A TTL above 30 days is interpreted as an absolute Unix timestamp. set key 0 2592001 5 does not mean "30 days and one second"; it means a timestamp back in 1970, and the item expires immediately.

Counting on INCR against a non-numeric value. Redis answers ERR value is not an integer or out of range; memcached answers CLIENT_ERROR cannot increment or decrement non-numeric value. Both are easy to trip after a serialisation format change.

Forgetting that FLUSHALL can stall. On a large keyspace the synchronous form blocks for seconds. Use FLUSHALL ASYNC — or better, scope your deletions and never flush a shared instance at all.

Trusting a benchmark run over loopback. Localhost numbers overstate both systems by removing the network, which is where most real latency lives. Measure across the same topology you will deploy.

Closing

Neither system is a general upgrade over the other. Memcached is a specialised tool that does one thing with unusually few sharp edges, plus an SSD tier that makes it dramatically cheaper at scale. Redis is a small data platform that happens to be an excellent cache, with a correspondingly larger set of ways to configure it badly. Decide by writing down three things — your object size distribution, your tolerance for a cold start, and whether any operation in your design must be atomic across more than one key. Those answers will pick the system for you far more reliably than any throughput chart.

Bekzod Erkinov

Bekzod Erkinov

Author

Founder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.

🎁 Free guide

Get the AI-Assisted Developer's Field Guide

The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.

Comments (0)

Please log in to leave a comment.

Log In

Related Articles

Don't miss the next deep dive

Get one well-researched tutorial in your inbox each week. No spam, unsubscribe anytime.