Memcached's Slab Allocator: Why Your Cache Wastes Memory on Purpose
Memcached's slab allocator trades memory utilization for predictable latency. Here's how slab classes work, why eviction is per-class, and how to measure your actual waste with stats slabs.
10 Sept 2026, 22:32 UTC

The problem: predictable latency beats perfect packing
If you run memcached in production long enough, you eventually open stats slabs and notice something odd: a chunk of your allocated memory is simply unused. Not leaked — deliberately idle. This isn't a bug. It's the price memcached pays to guarantee that a set never waits on the operating system's memory allocator.
Memcached's slab allocator is one of the cleaner examples of an engineering trade-off made explicit: sacrifice some memory utilization to eliminate fragmentation and allocation latency entirely. Understanding how it works explains several behaviors that otherwise look like bugs — evictions when the cache "isn't full," rejected large values, and memory that won't move where you need it.
How slabs actually work
At startup, memcached pre-allocates its memory pool and carves it into 1MB pages. Each page is assigned to a slab class, and every class has a fixed chunk size. Class 1 might hold 96-byte chunks, class 2 might hold 120-byte chunks, and so on, with sizes growing by a configurable factor (1.25 by default).
When you store an item, memcached finds the smallest chunk that fits the key, value, and a small amount of metadata, and drops the item in. When the item expires or is evicted, the chunk goes back on that class's free list — it is never returned to the OS. Because all allocation is internal list manipulation, memcached never calls malloc or free during normal operation. That means no external fragmentation over weeks of uptime and no jitter from the system allocator.
The cost is internal fragmentation: the empty space between your value and the end of its chunk. A 900-byte value in a 1024-byte chunk wastes about 12% of that chunk. Across millions of keys with varied sizes, the waste adds up.
A worked example: watching items land in classes
You can see this directly on a local instance. Start memcached in verbose mode (no special privileges needed for a non-default port):
memcached -p 11212 -m 64 -vvThen store a few representative values — say, a 200-byte session blob, a 5KB rendered fragment, and a 900KB JSON payload — using your client of choice. From any shell with nc or a memcached client, query:
stats slabs
stats items
The stats slabs output shows, per class, the chunk size (chunk_size), how many chunks are in use, and how many pages the class owns. stats items shows how many live items sit in each class. You'll see your 200-byte value land in a class with chunks slightly larger than 200 bytes, and the gap between the two numbers is your per-item waste. Multiply that gap by item count and you have a concrete estimate of fragmentation overhead for your real workload — far more useful than rules of thumb.
Two things worth checking in that output: evicted counts per class (nonzero means that class is under memory pressure) and total_pages distribution (a class hoarding pages it barely uses is a tuning target).
The sharp edges
Eviction is per slab class, not global. Each class has its own LRU. If your small-item class fills up, memcached evicts the least-recently-used item in that class even if a large-item class is sitting half empty. This is the classic "memcached evicted my keys but usage was only 60%" mystery. TTLs don't protect you here — memcached is a cache, and under pressure it will drop unexpired items.
Memory historically couldn't move between classes. Pages were assigned to a class permanently. If your workload shifted — say, average value size doubled after a deploy — the newly hot class would thrash while the old class idled. Newer 1.4.x-era and later releases added a slab rebalancer and automove to shift pages between classes, but behavior and defaults vary by version and may be disabled. Check memcached -h and your version's release notes before assuming it's active; older pinned deployments may not have it at all.
Big values are rejected outright. Anything larger than the maximum item size (1MB by default, tunable with -I) is refused. The usual fixes are client-side compression or splitting values into multiple keys — both of which change your size distribution, which loops right back into slab planning.
Tuning what you can
Two startup flags shape the class layout:
-f(growth factor): the multiplier between consecutive chunk sizes. Lowering it (e.g., from 1.25 toward 1.1) creates more classes with tighter size bands, reducing per-item waste — but more classes to keep supplied with pages.-n(minimum chunk size): where the smallest class starts. If your keys are tiny, the default minimum may waste space on every small item.
These are startup-only flags; changing them means a restart and a cold cache. And the defaults (-f 1.25, 1MB max item, 64MB total memory) are conservative placeholders — the -m 64 default in particular is almost never what you want in production.
The takeaway
Don't tune blind. Run stats slabs against a staging instance loaded with representative data, compute the actual waste per class, and check per-class eviction counts under realistic traffic. If one class is evicting while others idle, either rebalance your value sizes (compression often helps) or adjust -f so your common sizes sit near chunk boundaries. The slab allocator will happily trade memory for latency — your job is to make sure it trades the right amount.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.