The moment your Facebook feed loads, youβve just touched one of the most brutally optimized distributed systems on Earth.
You probably donβt think about it. You scroll. You double-tap. The app feels instant. But behind that millisecond response lies a war fought at the network layer, where Metaβs Memcached fleet handles more queries per second than all the worldβs search engines combined.
Let me show you what that actually looks like under the hood.
The Hook That Pulls You In
Imagine a system so fast that adding network latency between servers actually reduces throughputβnot because the network is slow, but because the CPU spends more time waiting for packet acknowledgments than processing data. Thatβs the kind of paradox you hit when youβre pushing trillions of requests per day through a single caching layer.
In 2023, Meta published a paper that quietly dropped a bomb on the distributed systems community. They revealed that their production Memcached cluster routinely handles 200 billion requests per second during peak traffic. To put that in perspective:
- Thatβs 2.3 million queries per millisecond
- Each query must complete in under 1 millisecond
- The cache hit rate consistently exceeds 99.6%
- The system has been running for over 15 years without a global outage
This isnβt just βscaling up.β This is rewriting the laws of physics for data access.
Why Memcached? Why Not Redis? Why Not Custom?
Hereβs the spicy truth: Metaβs engineers hate Memcached.
Theyβve said it publicly. The data structures are primitive. The memory allocator is a disaster at scale. Thereβs no built-in replication, no persistence, no security. By every modern standard, itβs a terrible choice.
But hereβs the thing: Memcachedβs simplicity is its superpower.
At Metaβs scale, predictable latency trumps advanced features. Redis with its rich data types introduces unpredictable CPU spikes. Custom systems introduce bugs. Memcached does one thingβO(1) key-value lookups over TCPβand does it so simply that engineers can reason about every single cache miss.
This is the first lesson of Metaβs architecture: Do one thing perfectly, then optimize the hell out of the surrounding infrastructure.
The Three-Layer Architecture That Defeated Physics
Let me break down the actual deployment topology. This isnβt theoreticalβthis is what runs in Metaβs fleet today.
Layer 1: The Frontend Farm (The βMcrouterβ Layer)
Every web server at Meta runs a local Mcrouter instance. This is Metaβs homegrown routing proxy that turns Memcached from a simple daemon into a distributed hash table with global coordination.
Web Server A
ββ Local Mcrouter (process)
ββ Memcached Pool 1 (US-East)
ββ Memcached Pool 2 (US-West)
ββ Memcached Pool 3 (EU)
Web Server B
ββ Local Mcrouter (process)
ββ Memcached Pool 1 (US-East)
ββ Memcached Pool 2 (US-West)
ββ Memcached Pool 3 (EU)
The critical design choice: Mcrouter runs as a process local to each web server, not as a standalone cluster. Why? Because every microsecond of routing overhead compounds.
- No additional network hopβMcrouter communicates with Memcached servers over the same rack switches
- In-process connection poolingβreuses TCP connections across all requests
- Dynamic topology discoveryβMcrouter learns server failures and pool assignments via a gossip protocol that converges in under 100ms
Layer 2: The Cache Tiers (Why You Need 3 Separate Pools)
Most engineers think βone big cache pool.β Meta runs three distinct tiers per region:
| Tier | Size | Latency Budget | Eviction Policy | Use Case |
|---|---|---|---|---|
| Regional | 100+ TB | <500ΞΌs | LRU | Hot user data, session state |
| Replica | 50+ TB | <1ms | LRU | Read replicas of hot keys |
| Frontend | 10+ TB | <100ΞΌs | None* | Pinned, never-evict data |
Wait, a tier with no eviction? Yes. The Frontend tier stores data that must never be evictedβlike authentication tokens and routing metadata. This is a radical departure from standard Memcached behavior. Meta achieved this by modifying the Memcached source to support key pinning with custom slab allocator policies.
Layer 3: The Shared Memory Secret Weapon
Hereβs where it gets wild. Meta doesnβt actually run Memcached as a standalone process on their web servers. They use shared memory regions to bypass the overhead of inter-process communication entirely.
Web Server Process
ββ Request Handler Thread
β ββ Maps to Shared Memory Region A
ββ Another Handler Thread
β ββ Maps to Shared Memory Region B
ββ Mcrouter Process
ββ Also maps to Region A & B
This means the web serverβs request handlers and Mcrouter both access the same in-memory cache without any serialization or copying. The data is just thereβa pointer away.
At this point, youβre not βgetting data from cache.β Youβre reading memory that was already cache-hot for your process.
The Network Stack: Where Most Systems Die
Memcachedβs original implementation uses a single-threaded event loop with epoll or kqueue. At Metaβs scale, thatβs a joke. Theyβve completely rewritten the I/O layer.
The Problem with Traditional Memcached I/O
Standard Memcached:
Client β TCP connect β SYN/ACK Handshake β Send request β Server parses β Send response
At 200 billion requests/second, the TCP handshake alone would consume all available CPU on the switching fabric.
Metaβs Solution: mutilate + Kernel Bypass
Meta developed mutilate (now open-source as part of their Memcached fork), which implements:
- UDP for reads, TCP for writes: Because 99% of traffic is reads, and UDP avoids the TCP slow start bottleneck
- Kernel bypass with DPDK: Data Plane Development Kit bypasses the kernel network stack entirely:
- No TCP stack processing
- No socket buffer copies
- Direct NIC-to-application memory mapping
- Connection batching: A single Mcrouter instance can send thousands of cache requests in a single send() call, amortizing system call overhead
The result? Per-core throughput increased from 500K req/s to 6M req/sβa 12x improvement without changing the Memcached protocol.
The Slab Allocator Nightmare (And How They Fixed It)
Memcachedβs slab allocator is famously fragile. It pre-allocates memory into slabs of fixed sizes (64B, 128B, 256B, etc.). If a key-value pair doesnβt fit neatly into a slab, memory fragmentation explodes.
At Metaβs scale, this was causing 20-30% memory waste. Their fix is a masterclass in practical engineering:
The Arena Allocator
Instead of fixed slabs, Metaβs Memcached uses arena allocation with dynamic slab growth:
// Simplified pseudo-code
typedef struct {
char* base; // Start of arena
size_t used; // Current usage
size_t capacity; // Max capacity
pthread_mutex_t lock; // Per-arena lock
void* last_alloc; // For fast O(1) freeing
} arena_t;
// On allocation:
void* arena_alloc(arena_t* arena, size_t size) {
pthread_mutex_lock(&arena->lock);
void* ptr = arena->base + arena->used;
arena->used += size;
arena->last_alloc = ptr;
pthread_mutex_unlock(&arena->lock);
return ptr;
}
This is dramatically simpler than the original slab allocator. It trades memory fragmentation for linear allocation speed. And because Metaβs workloads are dominated by small, uniformly-sized objects (session tokens, user IDs), fragmentation is minimal.
But hereβs the genius: They never free individual allocations. The arena is only reclaimed when an entire page (4KB) is evicted. This eliminates the need for garbage collection or defragmentation entirely.
Consistency: The Elephant in the Room
How does Meta handle cache consistency when you have 2000+ Memcached servers and every write must be instantly visible?
They donβt. At least, not in the traditional sense.
The βInvalidate, Donβt Updateβ Mantra
Metaβs rule is simple:
- Writes go to the database first
- The database asynchronously invalidates the cache
- Cache misses are acceptable; stale data is not
This means Mcrouter doesnβt use cache-coherence protocols like MESI. Instead, it tracks invalidation queues:
Database Write β Invalidation Queue (Redis Stream) β Mcrouter hears invalidation β Marks key as "maybe stale" β Next read triggers fresh fetch
The βLeaseβ Mechanism for Thundering Herds
When a popular key is invalidated (e.g., βtrending_video_123β), thousands of servers immediately request it from the database. This thundering herd can crush the DB.
Metaβs fix: Leases. When Mcrouter sees the first cache miss for a key, it returns a lease token to one server:
Server A: GET key β MISS β Receives lease token #42
Server B: GET key β MISS β Receives "LEASE_DENIED"
Server A: SET key value WITH TOKEN #42 β Cache updates
Server B: RETRY GET key β HIT
This ensures only one server ever goes to the database for a given key at a time. The lease token prevents stale writes from out-of-order responses.
The βNo Failuresβ Myth: How They Survive at Scale
Metaβs SREs love to say: βEverything fails, all the time.β At 200B req/s, you have:
- 1 server failure every 3 minutes (due to hardware faults)
- 2 network link drops per hour (from optical transceiver failures)
- Uncountable packet corruption (cosmic rays flipping bits in DRAM)
The βN+2β Redundancy Model
Instead of N+1 (standard), Meta uses N+2 for their cache pools:
Pool for "user_sessions"
ββ 10 active servers
ββ 1 standby (warm)
ββ 1 cold spare (offline, can be provisioned in 5 seconds)
The cold spare is a machine that isnβt even powered on. But because Metaβs orchestration system can PXE-boot and configure a server in 5 seconds, a cold spare is statistically always available before a second failure occurs.
The βRolling Degradationβ Protocol
When a cache server starts dying (e.g., memory errors increasing), it doesnβt just crash. It enters degraded mode:
- Stops accepting writes (stops growing its dataset)
- Serves reads only (still useful for 30-60 seconds)
- Drains its data (sends most frequently accessed keys to neighbors)
- Signals βdyingβ to Mcrouter, which re-routes traffic
This graceful degradation means no sudden traffic spikes to other serversβthe load is spread over minutes, not milliseconds.
The Hardest Lesson: Why Simplicity Wins
Iβve saved the most important insight for last. Metaβs engineers could have built a cutting-edge distributed cache with:
- CRDT-based conflict resolution
- Multi-master replication
- Automatic sharding
- Rich query capabilities
They didnβt. They kept Memcachedβs insane simplicity and spent all their engineering effort on:
- Network optimization (DPDK, batching)
- Memory efficiency (arena allocator)
- Failure resilience (leases, degradation)
- Observability (every cache miss is logged with a stack trace)
The result is a system where the core caching logic is 200 lines of C, but the supporting infrastructure is 50,000+ lines of auxiliary code.
This is the ultimate engineering lesson: Complexity belongs in the infrastructure, not in the protocol.
What This Means for Your Architecture
You probably donβt need 200 billion req/s. But Metaβs approach reveals principles that apply at any scale:
- Measure everything, then optimize the bottleneck. For Meta, it was network I/O. For you, it might be memory bandwidth or serialization overhead.
- Accept eventual consistency for read-heavy workloads. Cache invalidation is expensiveβdesign your system to tolerate it.
- Use leases or similar throttling to protect your database from thundering herds. Even 100 req/s can overwhelm a Postgres instance.
- Prefer process-local caching with shared memory over remote caches. The difference between a pointer dereference and a network round trip is 1000x in latency.
The next time you write a cache.get(key) in production, remember: youβre standing on the shoulders of a system that had to solve problems most engineers never even imagine.
And if your cache miss rate exceeds 1%, someone at Meta is probably having a very bad day.
Want to dive deeper? Metaβs engineering blog has the full paper on Scaling Memcached at Facebook. I also highly recommend exploring their open-source Mcrouter codeβitβs a masterclass in C++ network programming.
Whatβs your experience with scaling caches? Drop a commentβIβd love to hear about your war stories.