Comprehensive Guide to Memcached for Java Microservices
May 10, 2025
Great. I’ll compile a practical engineering guide on Memcached tailored for microservices environments with an emphasis on Java integration. The guide will cover architecture, core principles, command usage, implementation patterns, performance optimization, scaling strategies, security, and real-world case studies, with diagrams and Java code snippets.
I’ll let you know once the comprehensive guide is ready.
Comprehensive Guide to Memcached for Java Microservices
1. Introduction to Memcached
Memcached is a free, open-source, high-performance distributed memory caching system. It functions as an in-memory key–value store for small chunks of arbitrary data (strings, objects) from the results of database calls, API calls, or page rendering. In a microservices architecture, Memcached serves as a shared, transient data layer that multiple services can use to cache frequently accessed data, thereby alleviating load on slower backing stores (like databases) and reducing inter-service calls. Essentially, you can think of Memcached as a short-term memory for your applications – data that would otherwise require expensive computations or database reads can be retrieved quickly from memory.
Brief History: Memcached was originally developed by Brad Fitzpatrick for LiveJournal in May 2003 to accelerate dynamic web pages. It was first written in Perl and later rewritten in C for efficiency. Over the years, Memcached has become a cornerstone of web scaling, adopted by large platforms like Facebook, Twitter, Wikipedia, Pinterest, and Reddit. Its simplicity and speed made it the de facto standard for distributed caching in the Web 2.0 era. Key milestones include the introduction of multithreading and the binary protocol (around 2008), and more recent enhancements such as a new meta text protocol (2019) that improved efficiency and added features like anti-stampede controls. Major cloud providers (AWS, Google Cloud, Azure, etc.) now offer Memcached as a managed service, underscoring its continued relevance in modern architectures.
Role in Microservices: In microservices, each service often has its own database or expensive external calls. Memcached provides a unified caching layer that all services can share. By caching database query results, computed values, or session data in Memcached, microservices can serve repeated requests from memory instead of hitting databases or other services repeatedly. This reduces latency and inter-service chatter, and increases overall throughput and resiliency. Memcached’s distributed nature fits naturally with microservices: you can deploy a cluster of Memcached nodes that scale out horizontally as load increases, and clients (the microservices) will distribute cache entries across these nodes. The result is improved performance and reduced load on persistent stores, which is crucial as the number of microservices (and thus the number of database calls) grows.
2. Core Architecture & Components
Client–Server Model: Memcached follows a simple client–server architecture. A Memcached server is a daemon that stores data entirely in RAM and listens for client requests on a specified port (11211 by default, over TCP or UDP). Clients (your application or microservice instances) use a Memcached client library to communicate with the server(s). Importantly, Memcached servers are dumb in the sense that they do not coordinate with each other – there is no inter-server communication or replication by default. The clients are responsible for knowing the list of Memcached server addresses and deciding which server holds a given key. This design yields a shared-nothing horizontal scaling model: you can add more Memcached servers to increase cache capacity and throughput, and the clients will distribute data among them (more on how, in hashing strategies below).
Memory as a Distributed Hash Table: From the perspective of an application, Memcached provides a large hash table distributed across multiple nodes. When an application wants to store or retrieve a value, the Memcached client library will hash the key and map it to one of the available servers. That server then stores the key and value in its in-memory hash table. If the total cache is “full” (memory exhausted), inserting new data causes older data to be evicted using an LRU (Least Recently Used) policy on that server. Keys can be up to 250 bytes and values up to 1 MiB in size (by default). Each Memcached server manages its own portion of the hash table (its own subset of keys), and the client ensures that a given key always hashes to the same server (unless the server list changes). This way, all servers collectively act as one logical cache.
Protocols – ASCII vs. Binary vs. Meta: Interaction with Memcached can happen via two main protocols:
- ASCII Text Protocol: The original protocol where commands (like
set
orget
) and responses are sent as human-readable text. For example, a client might sendset myKey 0 3600 5\r\nhello\r\n
to store the value "hello". The text protocol is simple and widely supported. - Binary Protocol: Introduced in Memcached 1.4, the binary protocol sends commands and data in a compact binary format. It supports the same operations but can be more efficient in high-throughput scenarios and includes optional SASL authentication. However, as of recent memcached versions, the binary protocol is considered deprecated and no longer receives new features due to issues (security concerns, complexity, limited extensibility).
- Meta Text Protocol: Added around 2019, this is an extension to the text protocol that provides additional capabilities (such as better client control to mitigate stampedes, fetch stale data, etc.) while remaining human-readable. The meta protocol is now recommended for new clients as it is more efficient and extensible than the old binary protocol. (For typical usage, many client libraries still use the classic text protocol under the hood, but awareness of meta commands is useful for advanced scenarios.)
Most Java client libraries default to either the ASCII or binary protocol. For example, Spymemcached uses the ASCII protocol by default, while XMemcached can be configured to use the binary protocol (as shown later). Regardless of protocol, the basic caching behavior is the same – protocol differences mainly affect performance and features like authentication.
Internal Memory Management – Slabs and Chunks: One of Memcached’s core design points is its custom memory allocator, which avoids fragmentation by managing memory in fixed-size blocks:
- When a Memcached server starts, it allocates a fixed amount of memory (e.g.,
-m 1024
for 1024 MB) for item storage. This memory is not allocated item-by-item, but is instead divided into pages of 1MB each. - Each 1MB page is assigned to a slab class. A slab class represents a bucket of a certain object size. Within a page belonging to a slab class, the page is further carved into equal-size chunks to store individual items. For example, slab class 1 might hold small items (e.g. chunk size 80 bytes), slab class 2 slightly larger (e.g. 104 bytes), and so on, up to slab classes that hold 1MB items. Memcached by default creates slab classes with chunk sizes growing exponentially (factor of 1.25 by default) until the max item size (1MB) is reached.
- When you store an item, Memcached chooses the smallest slab class that can fit that item (including its metadata overhead). The item is placed into a free chunk in a page of that class. If no free chunk is available in that class, and there’s still free overall memory, Memcached will allocate a new 1MB page to that class (taking it from a global pool of unassigned pages).
- Once a page is assigned to a slab class, it never changes class. This means memory is essentially partitioned among different size categories. This design eliminates external fragmentation (small and large objects don’t compete for the same exact memory space) but can lead to some internal fragmentation (if an item doesn’t exactly fill its chunk, the rest of that chunk’s space is wasted).
Memcached “slab allocation” memory model: memory is divided into 1MB pages, which are assigned to slab classes (each slab class holds a specific chunk size). In this illustration, slab #1 holds smaller chunks (e.g. 256KB each) and slab #2 holds larger chunks (e.g. 512KB each). Green blocks represent cached objects filling some chunks; white space represents unused space in chunks or free chunks. Slab allocation avoids intermixing different-sized objects on the same page, reducing fragmentation. However, if the object size distribution shifts (e.g. many large objects now needed in slab #2 while slab #1 has free space), memory can appear “free” in one class but unavailable to another – potentially causing evictions in one slab class even while memory sits idle in another.
Each slab class maintains its own LRU list of items and its own set of usage statistics. In essence, Memcached’s memory allocator behaves as multiple mini-caches (one per slab class) to balance efficient memory use with simplicity. Modern Memcached (post-1.5.0) also includes background threads (“LRU crawler”) that proactively reclaim expired items from slabs to free space for new items, which helps make expiration behavior more predictable.
LRU Eviction: If a Memcached server runs out of free memory in the appropriate slab class for a new item, it will evict an item to make room (this is what makes it a cache). Eviction is done using an LRU policy on a per-slab-class basis. This means each slab class has an LRU linked list of its items (with recently used items at the head). The server will take the least recently used item (the tail of the LRU) in the needed slab class and evict it (freeing that chunk) to store the new item. Memcached will prefer to evict items that have expired (past their TTL) if it finds any at the LRU tail, otherwise it will evict the oldest unused item even if it hasn’t expired. Upon eviction or expiration, the memory for that item is recycled for new data. Importantly, because of the slab segregation, Memcached might evict an item in one class even while memory in another class is free but unsuitable for the item size needed. (Advanced note: Twitter’s fork Twemcache actually uses a different strategy called segmented LRU/slab eviction to mitigate this; see case study section.)
Connection Handling & Multithreading: Memcached is designed to handle a large number of concurrent connections from clients. It uses the libevent library for efficient I/O event notification and is fully multi-threaded to make use of multiple CPU cores. Each Memcached server process can run worker threads (you can configure the number with -t
option). Internally, there is one listener thread that accepts incoming connections and dispatches them to worker threads. Each worker thread runs its own event loop (via libevent) to read and write on its set of client connections. All worker threads share access to the common item cache (with locking around the global data structures as needed). This architecture allows Memcached to scale on modern multi-core machines and handle tens of thousands of concurrent client connections efficiently. The use of libevent and non-blocking I/O means a single thread can multiplex many client sockets. Facebook, one of the largest users of Memcached, even introduced optimizations like a shared connection buffer pool to reduce per-connection memory overhead and moved frequently to using UDP for reads to further boost throughput. In summary, Memcached’s server implementation is highly optimized for network and memory concurrency, making it capable of serving hundreds of thousands of operations per second on beefy hardware.
3. Operations & Commands
Memcached provides a simple set of commands for clients to perform cache operations. The core operations can be remembered as the basic “CRUD” on a cache: you set values, get values, and delete values by key. It also provides a few extras like counters and CAS (check-and-set). Below we detail common commands with examples (especially focusing on how to use them from Java):
-
SET: Store an item in the cache, unconditionally (always stores the new value, replacing any existing value for the key). Syntax (text protocol):
set <key> <flags> <exptime> <bytes> [noreply]\r\n<data>\r\n
. For example:set user:123 0 300 15\r\n{"name":"Joe"}\r\n
would store the JSON string{"name":"Joe"}
under keyuser:123
for 300 seconds. In Java with a client library, this is typically a method call likeclient.set(key, expiration, value)
. The flags field is a 32-bit number clients can use to tag data (often 0, unless used for type hints or compression markers). The exptime is the TTL (in seconds, or Unix timestamp for absolute expiry > 30 days). The bytes is the length of the data block to expect. The server replies withSTORED
on success. In Java (using SpyMemcached for example), the usage is asynchronous but simple: you can set a value like:MemcachedClient client = new MemcachedClient(new InetSocketAddress("localhost", 11211)); client.set("user:123", 300, "{\"name\":\"Joe\"}"); // (flags are usually handled internally or via a custom Transcoder)
-
GET: Retrieve the value for a given key. Syntax:
get <key1> [<key2> <key3> ...]\r\n
. The server will return any found keys with lines starting withVALUE <key> <flags> <bytes>
followed by the data and a finalEND
line. In practice, clients parse this and return the data. If a key is not found, it simply won’t appear in the response. The text protocol allows batching: you can request multiple keys in oneget
command (e.g.get user:123 user:456 user:789\r\n
) and the server will return all that exist. In Java, client libraries often provide a bulk get operation for convenience. For example, SpyMemcached hasclient.getBulk(keySet)
which returns a map of values. Example usage:String value = (String) client.get("user:123"); // or bulk get Map<String,Object> batch = client.getBulk("user:123", "user:456", "user:789");
A cache hit will return the stored object (or its deserialized form), while a cache miss returns null (in Java API terms) – meaning the key is not in cache.
-
ADD: Store an item only if it does not already exist. Syntax is identical to
set
(just use the keywordadd
). If the key already exists, the server repliesNOT_STORED
. This is useful for scenarios where you want to ensure not to overwrite existing cache entries. In Java:client.add(key, expiration, value)
will fail (return false or a failed Future) if the key is present. -
REPLACE: Store an item only if it already exists. If the key is missing, the operation does nothing (
NOT_STORED
). This can be used to update cached data while ensuring you don’t accidentally add new keys. (It’s less commonly used; one could just do set without harm if key not present.) For completeness, in SpyMemcached:client.replace(key, exp, newValue)
. -
APPEND / PREPEND: These commands append or prepend data to an existing value for a key (without re-sending the entire data). They require the key to exist (or they do nothing). Append adds bytes to the end, prepend to the beginning of the existing value. These are somewhat niche; in Java clients they exist but often you’d just get-modify-set instead.
-
GETS & CAS (Check-and-Set): Memcached supports an optimistic concurrency control via a CAS token. The CAS operation (compare-and-swap) allows you to update a value only if it hasn’t been changed since you last fetched it. The workflow is: you perform a “gets” (note the plural) to retrieve an item with its CAS identifier, then you perform a “cas” operation providing that identifier to attempt an update. The CAS id is a 64-bit unique value that memcached assigns to each stored item version. If another client modifies the data in between, the CAS id will have changed and your CAS update will fail. This is useful to avoid race conditions – e.g., two processes trying to update the same cached object concurrently. Example usage: In the text protocol, you’d do:
gets user:123 -> VALUE user:123 0 15 42\r\n{"name":"Joe"}\r\nEND (where 42 is the CAS token) cas user:123 0 300 17 42\r\n{"name":"Jane"}\r\n -> STORED
In Java (Spymemcached):
CASValue<Object> casVal = client.gets("user:123"); if (casVal != null) { Object currentValue = casVal.getValue(); long casId = casVal.getCas(); // modify currentValue as needed, then attempt CAS replace: CASResponse resp = client.cas("user:123", casId, modifiedValue); if (resp == CASResponse.OK) { // success } else { // failed, someone else changed the value } }
The CAS command in Memcached ensures atomicity: “store this data, but only if no one else has updated it since I last fetched it.” If the check fails, the client knows its update was not applied. This is analogous to an optimistic lock. Use CAS only when needed, as it adds an extra network round-trip (gets before cas) and some overhead.
-
DELETE: Remove an item from the cache before it expires. Syntax:
delete <key> [noreply]\r\n
. The server repliesDELETED
if the key was present (and removed) orNOT_FOUND
if the key didn’t exist. Deleting is straightforward via Java clients:client.delete(key)
. -
INCR/DECR (Counters): Memcached can treat values as unsigned 64-bit integers and supports atomic increment and decrement operations. For example, if you
set counter 0 3600 1
with data5
, then aincr counter 2
will change it to 7 (and return 7). If the key doesn’t exist, incr/decr can optionally return NOT_FOUND (or some clients allow specifying an initial value). In Java, methods likeclient.incr("counter", 2)
orclient.decr("counter", 1)
exist (often with overloads to specify default value if not present). These operations are useful for rate limiting, counters, etc., but note that the value is stored as text – the server just interprets it for incrementing. -
FLUSH_ALL: A command to invalidate all data in a Memcached server (optionally with a delay). This is rarely used in production (as it clears the entire cache), but is useful in testing or deploying new code when you want to flush old data. In practice, in a microservices environment, you might avoid flush_all in favor of more targeted cache key versioning or deletions.
-
STATS: Memcached servers provide a wealth of statistics accessible via the
stats
command (and related stats commands). For example,stats
returns general stats (uptime, memory used, evictions, hits, misses, etc.),stats items
returns per-slab-class item counts and age,stats slabs
returns memory info per slab, etc. These are typically fetched via the telnet interface or programmatically to monitor the cache’s health.
From a Java developer’s perspective, you typically won’t issue text protocol commands by hand. Instead, you’ll use a Java client library which provides methods for these operations, handling serialization/deserialization and the network protocol details internally. Two popular Java clients are:
- SpyMemcached (net.spy.memcached): A widely used asynchronous Java client. It is non-blocking and uses a single thread with NIO to manage connections, making it fairly lightweight on threads (callbacks or futures are used for async results). It supports the basic text protocol and some binary protocol support. SpyMemcached is easy to use – as shown, a simple
MemcachedClient
can connect to a list of addresses and you callget
,set
, etc. - XMemcached (com.googlecode.xmemcached): Another popular client, which is more fully featured and, in practice, often faster and more robust under high load. XMemcached uses a thread pool (one thread per memcached connection by default) and can leverage the binary protocol. It also supports consistent hashing and has features like failure detection, etc. MemCachier (a cache service provider) in 2018 recommended switching to XMemcached due to some observed bugs and edge-case issues with SpyMemcached (e.g., SpyMemcached not reconnecting after a server restart in certain cases).
Later in Section 4, we will compare these clients and show code. Regardless of client, the set of operations available is the same, corresponding to the commands above.
Batch Operations: When needing to fetch or set many keys, it’s more efficient to batch them rather than loop one by one. The text protocol allows multi-key get
as described. Some clients may also pipeline operations. For instance, you could initiate multiple async gets in parallel. Memcached does not have a multi-set in one command (you’d just send several sets). But libraries can hide latency by pipelining (sending next command without waiting for previous response). When dealing with dozens or hundreds of keys (e.g., caching a batch of user objects), batch retrieval drastically reduces round-trip overhead. Facebook’s use of Memcached heavily relies on multi-get to batch hundreds of keys per request to reduce network round trips. Just be cautious: extremely large multi-get requests (thousands of keys) can strain server CPU and network (this was termed the “multiget hole” at Facebook – where too many keys per request caused CPU bottlenecks on servers). A balanced approach (batch, but not too large batches) is best.
Expiration Policy: Every cache entry in Memcached can have an expiry time (TTL). When you set an item, you provide an expiration in seconds (or a Unix timestamp). If the expiration is set to 0, the item can live indefinitely (until evicted for space). Items that expire are not immediately purged at that exact moment; instead, they simply become unavailable for retrieval (a get will treat it as a miss, and a subsequent set can reuse the memory). Memcached post-1.5.0 runs a background LRU crawler that will eventually remove expired items from memory to free space. If a client requests a key that is expired, Memcached will notice it and treat it as not found (and free it). This lazy expiration means an expired item might linger until either requested or until the crawler reclaims it. In practice this is fine, but be mindful that if you set huge numbers of items with a short TTL, you should ensure the crawler is running (it is by default in modern versions) to avoid memory bloat from expired items. The expiration times also can be used strategically to invalidate cache entries after known periods.
Eviction Strategy: As mentioned, Memcached uses LRU eviction per slab class. This means least recently used items (that are not expired) are the first to go when new space is needed. This is generally suitable for cache (temporal locality). Memcached does not let you choose alternative eviction policies (unlike some caches that offer LFU, etc.), but it internally segments the LRU into “hot”, “warm”, and “cold” segments to reduce eviction of recently added items under certain conditions (this is an implementation detail for efficiency). For most users, Memcached eviction is effectively LRU. If you see a high rate of evictions in your stats, it indicates your cache is running at capacity and constantly kicking out older data to store new data. You might increase memory or consider if the slab classes are balanced (some items maybe using a lot of space with low reuse).
In summary, Memcached’s API is intentionally simple. For Java integration, it means the learning curve is small – you basically connect and use get/set like a map. The challenge is less in the API and more in choosing what to cache and how to manage cache invalidation and consistency, which we will address next.
4. Implementation Strategies & Integration (Java Focus)
Caching in microservices can be applied in various patterns. The primary caching strategies are often categorized as cache-aside, read-through, write-through, or write-back. We’ll explain each, and how you might implement them using Memcached (with Java examples), as well as discuss practical issues like session caching and preventing cache stampedes/penetration.
-
Cache-Aside (Lazy Loading): This is the most common pattern with Memcached. The cache sits on the side and the application is responsible for adding or evicting data. On a cache miss, the application fetches data from the database (or service), then stores it in the cache for next time, and returns the result. On a cache hit, it simply returns the cached data. The cache does not automatically know when data changes in the DB; the application must invalidate or update cache entries when underlying data changes. Implementation in Java is straightforward:
// Pseudocode for cache-aside read User getUserById(String userId) { User user = (User) cacheClient.get("user:" + userId); if (user != null) { return user; // cache hit } // cache miss: user = database.queryUser(userId); if (user != null) { cacheClient.set("user:" + userId, CACHE_TTL, user); } return user; }
And for write/update:
void updateUser(User user) { database.updateUser(user); cacheClient.delete("user:" + user.getId()); // invalidate stale cache }
This pattern (application-aside) gives the application full control. It’s easy to implement and is widely used. The downside is the code has to handle cache misses logic everywhere, or you abstract it nicely. Many frameworks (like Spring Cache) essentially implement cache-aside under the hood (they call the method and populate cache transparently).
-
Read-Through: In a read-through cache, the application always asks the cache for data. If the cache doesn’t have it, the cache itself loads it from the backing store and returns it (while populating the cache). In other words, the cache is inline for misses. Memcached by itself is just a dumb store – it doesn’t know how to load from your database. But you can implement a read-through behavior in your application by using a library or an abstraction. For example, you might use Spring’s
CacheManager
with aCacheLoader
that on cache miss will invoke a method to get data and put it in cache. In practice, with Memcached in Java, you’d either implement cache-aside in code (which is effectively read-through from the caller perspective), or use a higher-level caching library (like Google Guava’s caching library or Caffeine) in front that delegates to Memcached as a store. Some Java caching libraries (JSR-107 JCache implementations) can use Memcached as the backing store, giving a read-through illusion. The benefit of read-through is convenience – you decouple cache fetch and load logic. The drawback is complexity: you need a loading mechanism or use an existing solution.For example, using Spring Cache with a custom cache resolver, you might configure it so that if
cache.get(key)
returns null, it callsdatabase.fetch
and then doescache.put(key,value)
automatically. Memcached would just store whatever is provided. So while Memcached is not inherently a read-through cache, your integration can achieve the same effect. -
Write-Through: In a write-through cache, any time you update the database, you also synchronously write to the cache. The cache sits in-line for writes, ensuring the cache is always up-to-date with the latest data. For instance, when a microservice updates a user record, it would simultaneously update Memcached. If using write-through, the pattern might be: update DB, and on success, also do
cache.set(key, newValue)
. Next read of that data can be served from cache (no stale data because we wrote through). The benefit is simplicity for reads (no stale data if every writer follows the protocol). However, write-through adds latency to writes (each write now does two operations: DB and cache) and if either fails you have to handle it. In Java, implementing write-through might look like:void updateUser(User user) { database.updateUser(user); cacheClient.set("user:" + user.getId(), CACHE_TTL, user); }
Here we update the cache with the new state immediately. If the update is frequent and the cached data is large, this could be overhead – consider whether caching such frequently changing data is needed. Often, cache-aside with explicit invalidation (delete) is used instead, to avoid writing cache on every update.
-
Write-Behind (Write-Back): In this pattern, the application writes to the cache first (updating the cache value) and defers or batches the write to the database. The idea is low-latency writes and possibly reducing DB load by coalescing multiple updates. However, implementing write-behind with Memcached is not trivial – Memcached has no mechanism to later sync changes to the database. If you write to Memcached and not immediately to the DB, you risk losing that data if the cache evicts it or restarts. Typically, write-behind is done with caches that have persistence or event hooks (e.g., Redis or a local cache that logs modifications). Memcached doesn’t provide event hooks for eviction. Therefore, pure write-behind is not recommended with Memcached unless you build a custom layer that tracks dirty keys and flushes them to the DB. That complexity is usually overkill. In microservices, if you needed write-back caching, a product like Couchbase (which evolved from memcached to add persistence) or Redis with AOF might be more suitable. In summary, Memcached is usually used in write-through or cache-aside modes, not write-behind.
State/Session Management: A common use of Memcached in web microservices is to store user session data or other state that needs to be shared across stateless service instances. For example, if you have a cluster of stateless application servers (microservice instances), and you want user session or authentication tokens to be available regardless of which instance serves the request, you can use Memcached as a distributed session store. Many web frameworks have modules to use Memcached for HTTP session storage (e.g., Tomcat had a Memcached session manager, PHP has memcached session handlers, etc.). By storing session objects in Memcached, each service instance can quickly retrieve session info by session ID, without sticky sessions or hitting a database on each request. The pattern is: on login, store the session data in memcached; on each request, fetch session from memcached; update it as needed (write-through on session modification). This makes scaling out the service easier (any instance can serve any user). The caution here is that Memcached is not persistent – if it restarts or data is evicted, sessions could be lost. For things like session data, that might just mean the user needs to log in again, which is acceptable in many cases. If not, you’d consider a persistent cache or backing up session in DB as well.
Database Integration and Consistency: When using Memcached as a cache in front of a database, one of the hardest issues is cache coherency – ensuring the cache doesn’t serve stale data after the database is updated. There are a few best practices to manage this:
- Modify cache after successful DB commit: Always apply changes to the database first (since it’s the source of truth), then update or invalidate the cache. If the DB write fails, you’ve not corrupted your cache with new data. If the DB write succeeds but cache update fails (rare, but memcached could be temporarily down), you at worst serve slightly stale data until someone fixes or it naturally expires.
- Cache Invalidation vs Update: You can either delete the cache entry on a DB update (cache-aside strategy; next read will miss and fetch fresh from DB), or update the cache entry with the new value (write-through strategy). Deleting is simpler and avoids potential race conditions (like an update happening while another thread is reading – although CAS could handle that). However, deletion opens a small window for a race condition known as “cache stampede on miss with stale data”: Consider two requests A and B. A deletes the cache then updates DB. B comes in immediately and finds cache missing, then queries DB (still old data if transaction hasn’t committed or if B read from a replica with lag) – this can reintroduce stale data to cache. One common defensive approach is the “delete after write” and “short delay double delete”: delete cache, commit DB, then maybe delete cache again a few milliseconds later to catch any race. This is complex but sometimes used. Alternatively, some simply update the cache with new data (ensuring consistency immediately). The right approach depends on your consistency needs and traffic patterns.
- Use of CAS for concurrency: If multiple processes may update the same data, using Memcached’s CAS can help avoid lost updates in cache. For example, you can
gets
the object, apply changes, trycas
. If it fails, fetch again and reapply. However, remember you also have the DB – often simpler is to update DB and then just invalidate cache, relying on subsequent read-through to repopulate. - Cache aside on read, delete on write is a very common, stable pattern: reads populate cache, writes immediately invalidate. This ensures that after a write, no stale data will be served (the first read after will go to DB). The slight downside is the next read pays the DB cost (cold cache for that key). If that’s a concern, you could do write-through (update DB and cache).
Cache Stampede, Penetration, and Avalanche Mitigation: These terms refer to potential issues in caching systems that one should guard against:
-
Cache Stampede (Thundering Herd): This happens when a cache entry expires or is evicted, and then many concurrent requests all miss the cache and try to load the data from the database at the same time. This can overwhelm the database. For example, suppose a very popular item’s TTL expires at time T; at time T+1, 1000 requests come in, all see a miss and all query the DB. This defeats the purpose of caching and can crash the DB. To mitigate stampedes:
- Mutex/Locking: Only allow one thread (or one service instance) to fetch the data on miss, while others wait. Some implementations use a distributed lock or a short-lived "busy" cache entry. Memcached itself doesn’t have a locking primitive, but one approach is to store a special token value (e.g., store a value like “LOCK” with short expiry) using add (which succeeds only if not exists). If one client successfully adds a lock, it proceeds to fetch from DB, while others either pause or return slightly stale data. After fetching, update cache and remove lock. Libraries exist to abstract this.
- Stale-While-Revalidate: A technique where you serve stale cached data to some users while one background process updates the cache. Memcached’s new meta protocol has some support for fetching an item with a signal that it’s stale but should be updated by one caller. In absence of that, you can implement by storing not just a value but also perhaps a secondary key that indicates if an update is in progress.
- Jittered Expiry: To avoid many items expiring at the same moment (cache avalanche), add a random small offset to TTLs when setting items. This staggers expirations so you don’t get huge traffic spikes at exact intervals. For instance, instead of all items expiring exactly on the hour, they expire over a 5-minute window around that hour.
- External Tools: There are libraries like
critical-section
in some languages to handle this, or using an in-memory lock in each service instance if that’s acceptable.
-
Cache Avalanche: This refers to a scenario where a large portion of the cache expires or is wiped at once, causing a flood of misses. For example, restarting all memcached nodes (or a cluster-wide flush) could send all traffic to the DB suddenly. This is similar to a stampede but across many keys. Solutions include the jittered expiry as mentioned, and ensuring high availability of the cache (don’t lose all nodes at once; see next section on HA). Additionally, one can pre-warm the cache on startup (e.g., load some frequently used keys asynchronously).
-
Cache Penetration: This means lots of requests for keys that will never be in the cache (e.g., querying non-existent IDs, or perhaps even malicious random keys). Each request misses and then tries to hit the DB, which also returns nothing – but the cache isn’t populated (since there’s nothing to store), so the next request does it again, effectively bypassing the cache. This can generate unnecessary load on the DB. To mitigate this, one strategy is to cache negative results (cache the fact that “this key is not found”). For example, if a user ID 999 doesn’t exist, you can store a special value like
NULL
or a placeholder in cache with a short TTL (perhaps 60 seconds) so that subsequent requests for ID 999 hit the cache and see “not exists” without hitting the DB. This is a trade-off (using cache space for nonexistent data, and you must distinguish a real object vs a cached null). Alternatively, use a Bloom filter in front of the cache to quickly test if a key is known to be nonexistent. For instance, maintain a Bloom filter of all valid IDs. If a request comes for an ID not in the filter, you can immediately return not found (or 404) without touching DB or cache. Bloom filters have false positives, but that just means you might sometimes still check a key that isn’t there – which is acceptable at low rate if tuned.
In Java, implementing these protections might involve additional components like Redis (which has Lua scripting to implement locks, etc.) or using Memcached’s CAS and add commands cleverly. For example, to implement a lock using Memcached, you could do:
String lockKey = "lock:someResource";
boolean gotLock = client.add(lockKey, lockTTL, "1"); // add succeeds only if not exists
if (gotLock) {
try {
// Perform DB load and cache update
} finally {
client.delete(lockKey);
}
} else {
// Another process is doing it; either wait and retry, or return stale data
}
This is a simplistic illustration. In a microservice environment, one might use a distributed coordination service if strict serialization is needed. Often, careful use of TTLs and maybe slightly tolerating stale data for a brief moment can simplify things.
Integrating with Databases: A key pattern is caching database query results with Memcached. For example, if you have a microservice endpoint /users/{id}
that fetches from a SQL or NoSQL database, you can incorporate Memcached such that:
- On request, try
get("user:{id}")
from Memcached. - If hit, return the data (perhaps after deserialization).
- If miss, query the database (e.g.,
SELECT * FROM users WHERE id=?
). - Take the result, set
user:{id}
in Memcached with some TTL (maybe 5 or 10 minutes or more, depending on how fresh data needs to be). - Return the result.
This basic cache-aside around DB queries can dramatically reduce database load, especially for read-heavy workloads. Many microservices are read-mostly, and Memcached effectively offloads those reads. For write-heavy scenarios, caching might be less beneficial (or you cache something like computed aggregates or frequent reads of the write).
One should also consider data consistency: If other applications or services can modify the same data, you need a strategy so that such modifications also update/invalidate the cache. In a microservices context, this might involve event-driven cache invalidation. For example, if Service A caches product info, and Service B (or an admin tool) updates a product in the database, Service B could publish an event (via Kafka or a message broker) that Service A listens to in order to invalidate the product cache. Lacking that, one service wouldn’t know the DB changed behind its back, leading to stale cache until TTL expiry.
Java Client Libraries and Best Practices: When using Memcached in Java, choosing a good client and following best practices is important:
-
Choosing a Client: As mentioned, SpyMemcached and XMemcached are two major choices. SpyMemcached is asynchronous and single-threaded, which can be very efficient for high concurrency as long as your application can handle the asynchronous nature (it returns
Future
objects for operations, or you can use the blocking get on those futures). XMemcached is synchronous by default (though it also can do async) and creates multiple connections to each server, which can improve throughput on multi-core systems. XMemcached also has some nice features like automatic node failure handling and consistent hashing built-in. According to a MemCachier benchmark, SpyMemcached had better performance for simple get/set under certain conditions, but XMemcached shined in other areas (and in real-world use, SpyMemcached had some reconnection issues as noted). In 2018, MemCachier switched their recommendation to XMemcached for better reliability. There are also other clients:- Folsom: A newer client (used at Twitter in the past) focusing on performance, used with Finagle.
- Redisson Memcached compatibility: (if you ever wanted a unified Redis/Memcached interface, not common).
- Spring Simple Cache + Memcached: Projects like Spring Memcached or memcached-spring-boot-starter exist to integrate Memcached as a Spring Cache provider.
For our purposes, SpyMemcached and XMemcached suffice. Comparison Summary:
Feature SpyMemcached XMemcached Threads & Concurrency Single-threaded NIO (async futures), non-blocking operations. Multi-threaded (one per server connection by default), blocking ops (with async optional). Protocol Support ASCII (default) and partial binary. ASCII or Binary (configurable) with SASL auth support. Consistent Hashing & Sharding Yes (client handles hashing keys to servers; uses array mod by default, or Ketama consistent hashing if configured). Yes (supports consistent hashing, and even weighting of servers). Performance Very low overhead per operation; good for large number of connections. Known to have issues under certain failure conditions (e.g. doesn’t always reconnect on cluster changes). Excellent throughput, especially under high load or larger values. Tends to handle failover and reconnection more gracefully. Development Status Last major update a few years ago (still works for what it does). Actively maintained as of mid-2010s (version 2.4.x around 2018). Best Practice: whichever client you use, reuse the Memcached client instance. These clients manage their own thread(s) and connections. You should not create a new client object for every request – that would be extremely slow and exhaust file handles. Typically, create the Memcached client once (e.g., at application startup, perhaps via a Singleton or as a Spring bean) and use that throughout. The clients are thread-safe (for SpyMemcached you must use the async design properly; XMemcached’s object is thread-safe as well). Also, ensure to shut it down gracefully on application shutdown to close connections.
-
Connection Pooling: With SpyMemcached, it’s asynchronous so the concept of a pool of connections is abstracted (it uses one or few connections per server and multiplexes requests). XMemcached by default uses one connection per memcached server per XMemcached thread. It does allow configuring more if needed via its
MemcachedClientBuilder.setConnectionPoolSize()
. Usually, you don't need a large pool unless your single connection becomes a bottleneck; Memcached connections can handle a lot of async requests. Monitor latency to decide. -
Error Handling: Memcached operations can occasionally fail (for example, time out if the server doesn’t respond in time, or if a server node is down). Be prepared to handle exceptions or return nulls. Both clients allow setting timeouts on operations. For instance, with XMemcached you might catch
TimeoutException
,MemcachedException
,InterruptedException
around operations. In a microservice, a cache miss or error should ideally degrade gracefully to a DB call rather than crash the request. So have fallback logic if cache isn’t reachable (maybe log a warning and go to DB). -
Serialization: Memcached stores bytes. Java clients will by default serialize objects (often using Java serialization by default, which is not very efficient). SpyMemcached and XMemcached allow plugging in custom Transcoders or serializers. For instance, you might use a JSON library or Kryo or even store compressed data. If you store simple strings or use a standard codec, that’s fine. Just remember that if you are storing complex objects, using Java’s native serialization can be slow and produce large payloads – consider using a faster serialization (there are Transcoder implementations available, or you can store JSON manually). Also, different clients are not interoperable in how they serialize (unless you stick to plain text or handle it). If multiple services in different languages access the same Memcached data, use a common data format (like JSON or MsgPack).
-
Key Design: Choose keys carefully. Keys are arbitrary strings up to 250 bytes, but shorter keys mean less bandwidth. A common pattern is to include the object type and identifier in the key (as we did with
"user:123"
). This helps avoid key collisions and makes it clear what the key represents. In microservices, ensure keys are unique enough so that two different caches in the same Memcached cluster don’t accidentally overlap. If you have a single memcached cluster shared by multiple services, prefix keys per service or domain (e.g.,"inventory:item:1001"
vs"order:1001"
clearly separate an item vs order with id 1001). There’s no built-in namespace segregation in Memcached, so it’s up to your key naming. -
Max Key Size and Object Size: Remember the limits – keys < 250 bytes, values < 1MB (by default). If you try to store larger, the set will fail (and return NOT_STORED or throw exception in client). In microservices, if you need to cache something bigger than 1MB, you should probably rethink (that’s a lot of data per cache entry and might be better off in a different store or split). You can increase item size limit by starting memcached with
-I <size>
(e.g.,-I 2m
for 2MB), but that increases memory fragmentation potential. -
Compression: Many clients auto-compress large objects (e.g., if object > certain size, compress before storing and set a flag). SpyMemcached does this by default for objects beyond a threshold using GZip, to save space. Keep that in mind – CPU tradeoff for space. You might want to tweak or disable it if CPU is precious and you have plenty of RAM.
Example – Using XMemcached in Java: (with authentication and binary protocol, as might be needed for a cloud service)
import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.auth.AuthInfo;
import net.rubyeye.xmemcached.command.BinaryCommandFactory;
import net.rubyeye.xmemcached.utils.AddrUtil;
List<InetSocketAddress> servers = AddrUtil.getAddresses("cache1.example.com:11211 cache2.example.com:11211");
// If using a service like MemCachier or other SASL-authenticated cache:
AuthInfo authInfo = AuthInfo.plain("username", "password");
XMemcachedClientBuilder builder = new XMemcachedClientBuilder(servers);
// Setup auth on each server
for(InetSocketAddress addr : servers) {
builder.addAuthInfo(addr, authInfo);
}
// Use binary protocol for efficiency
builder.setCommandFactory(new BinaryCommandFactory());
// Optionally tune timeouts and connection pool
builder.setConnectTimeout(1000); // 1s connect timeout
builder.setHealSessionInterval(2000); // reconnect interval
MemcachedClient xClient = builder.build();
// Basic usage
xClient.set("foo", 0, "bar"); // store "bar" under "foo"
String val = xClient.get("foo"); // retrieve the value (should be "bar")
xClient.delete("foo"); // delete the key
This example shows connecting to a cluster with two nodes, enabling SASL auth and binary protocol, then performing basic ops. In a typical microservice, you’d wrap such client usage in a DAO or cache utility class.
Handling Cache Penetration in Code: As an example, to cache negative lookups (nonexistent data):
User getUserById(String id) {
Object cached = cacheClient.get("user:" + id);
if (cached != null) {
if (cached instanceof NullMarker) {
return null; // cached "not found"
}
return (User) cached;
}
User user = database.fetchUser(id);
if (user == null) {
// store a placeholder for not found
cacheClient.set("user:" + id, 60, NullMarker.INSTANCE);
return null;
} else {
cacheClient.set("user:" + id, 300, user);
return user;
}
}
Here, NullMarker
is just a singleton object to represent a null (could also just use a specific flag in a JSON, etc.). The idea is to prevent repeated DB hits for ID that doesn’t exist. Bloom filter approach would be different – you’d check the filter and perhaps return quickly if not present, or decide to not even attempt caching those.
In summary, integrate Memcached by abstracting it behind a service or repository layer. Hide the caching logic from your core business logic if possible. That makes it easier to change strategies. Also monitor the cache’s effectiveness: metrics like hit rate and read/write rates (we cover in next section) will tell you if your caching strategy is working or if you need to adjust TTLs, etc.
Memcached pairs well with microservices that are deployed in cloud environments – often you’ll use a managed cache service and connect to it as demonstrated, or run a Memcached cluster alongside your services (in containers or VMs). Next, we’ll look at performance tuning and scaling considerations.
5. Performance Optimization & Scalability
One big reason to use Memcached is performance – it can significantly speed up data access. But to get the most out of it, you should be aware of key performance metrics, and strategies to scale and tune the cache cluster. Here we discuss how to monitor Memcached, how data is distributed (hashing), and how to scale horizontally.
Key Performance Metrics: Monitoring your Memcached nodes is crucial. Important metrics and stats include:
- Cache Hit Ratio (Hit Rate vs Miss Rate): The proportion of cache lookups that result in a hit. Memcached’s
stats
provides countersget_hits
andget_misses
. Hit ratio =get_hits / (get_hits + get_misses)
. A high hit ratio (e.g. > 0.9 or 90%) means the cache is effectively serving most requests from memory. A low hit ratio means either your cache is too small, your TTLs are too low (data expires before reuse), or the data access pattern is not leveraging cache well. Monitor misses to decide if you need to allocate more memory or cache additional data. - Throughput (Ops/sec): The number of operations (gets/sets) served per second. This can be thousands to hundreds of thousands. Tools like
memcached-tool
or stats can show instantaneous ops. Ensure your network and CPU can handle your peak throughput. Memcached is often network-bound (especially on 10G networks it can push a lot). If using cloud, watch network egress if it costs money. - Latency: How fast is each operation? Memcached operations are usually sub-millisecond within the same datacenter. However, adding network hops (like cross-region) can add latency – always deploy your cache close to your services. If using a client with timeouts, watch how often timeouts occur. Increased latency could indicate an overloaded server or network issues.
- Memory Utilization: Check
bytes
(how many bytes used) andlimit_maxbytes
(max memory configured) from stats. Ideally, you want to use as much as possible (to maximize cache space) but not exceed (memcached won’t exceed limit_maxbytes except a tiny overhead). Ifbytes
is constantly atlimit_maxbytes
andevictions
are high, you might benefit from more memory or additional nodes. - Evictions: The counter
evictions
tells how many items have been evicted (LRU) to make room. Some evictions are normal in a healthy, full cache (that’s what it’s supposed to do). But if evictions are very high and growing quickly, it means items are being kicked out before they naturally expire, likely due to capacity constraints. High evictions, combined with misses, might indicate thrashing (data can’t stay long enough to be reused). Solutions: increase cache size or tune item TTLs (maybe some data with low reuse shouldn’t be cached at all to save space for hotter data). - Connections:
curr_connections
vsmax_connections
. Ensure you aren’t hitting connection limits. Memcached can handle many connections, but each costs some memory. If you see an unexpectedly high number of connections, maybe some clients aren’t reusing connections properly. - CPU usage on Memcached nodes: If memcached CPU is saturated (e.g., one core pegged at 100%), throughput might plateau. Because memcached is multi-threaded, it can use multiple cores, but if your workload is heavily skewed (like one very hot key might become a contention point under lock). Usually, memcached is memory or network bound, not CPU, unless using very fast networks or doing a lot of serialization in the client.
- Network Traffic:
bytes_read
andbytes_written
stats show how much data. If you have large values, those will dominate network. Keep an eye on your network capacity.
Collecting these metrics: Many monitoring systems have Memcached integration (Datadog, Prometheus, etc., can pull stats via the text protocol or through an exporter). AWS ElastiCache publishes CloudWatch metrics for hits, misses, evictions, CPU, network, etc.. Use these to know if your cache is under pressure or underutilized.
Consistent Hashing & Data Distribution: By default, Memcached clients use a hashing algorithm (often a CRC32 or MD5 based hash) modulo the number of servers to pick a server for a given key. This means if you have N servers, each key K
goes to server hash(K) mod N
. This simple scheme is efficient but has a downside: if you add or remove a server, almost all keys get remapped (because N changes, so mod result changes for most hashes). This causes a massive cache miss spike whenever the cluster membership changes (essentially a cache flush across the board).
To mitigate that, consistent hashing is used. Consistent hashing doesn’t depend directly on N; instead it maps both servers and keys onto a hash ring space (0 to 2^32 - 1 for example) and each key goes to the next server on the ring. When a server is added or removed, only a subset of keys change ownership (ideally proportionate to 1/N of keys). Many Memcached clients implement consistent hashing – a well-known algorithm is Ketama (developed by Last.fm). With Ketama, servers are hashed to many points on the ring (to balance load), and keys then find the nearest server point clockwise. The result is minimal disruption when the server list changes.
Java clients:
- SpyMemcached has a
KetamaConnectionFactory
you can use to enable consistent hashing (otherwise it defaults to array mod). It also has the ability to auto-rehash if a server goes down (depending on how you configure theFailureMode
). - XMemcached uses consistent hashing by default (it calls it “HashAlgorithm.KETAMA_HASH” for instance) and will smoothly handle node changes.
For microservices, consistent hashing is important if you plan to dynamically scale the Memcached cluster up or down. In containerized environments (Kubernetes etc.), if pods come and go, you’d want consistent hashing to avoid huge cache turnover. If using a managed service where the number of nodes is fixed or changes rarely, it’s still recommended to use consistent hashing to handle failovers smoothly.
Hashing and Hot Keys: Also consider that simple mod hashing can lead to uneven distribution if not all keys are equally likely. Consistent hashing also generally distributes keys evenly if your hash is good. If you have a hot key (one key that is extremely popular), Memcached itself cannot split that key’s load across servers (since one key maps to exactly one server). That server can become a bottleneck (hot shard problem). Solutions for hot keys are application-level (e.g., replicate that data onto multiple keys/servers or cache at client side). Memcached has no built-in replication to share the load of one key across nodes (unlike a system that could partition a single dataset).
Network Tuning: Memcached communication is typically done over TCP. A few network considerations:
- LAN usage: Keep your cache close. If services and Memcached are in the same data center or cloud region, latency will be low. Avoid cross-region calls for each cache lookup.
- Nagle’s Algorithm: Some clients disable Nagle (TCP_NODELAY) to avoid delays for small packets, since Memcached operations are often small. This reduces latency for requests that might otherwise wait for the Nagle timer. Most Memcached clients take care of this (e.g., SpyMemcached sets TCP_NODELAY on sockets).
- UDP: Memcached supports UDP for gets (and multi-get). UDP can avoid the overhead of TCP for some scenarios (no connection, less packet overhead), but it has its own challenges (packet loss, ordering). Facebook famously used UDP for multi-get queries to reduce kernel TCP overhead and implement their own flow control. For most users, sticking to TCP is fine, because it ensures reliability. UDP might be considered in an environment with extremely high read load and low network loss (maybe internal data center with custom logic to handle drops). If using UDP, clients need to handle packet reassembly for large multi-gets and potentially missing responses.
- Connection Limits and Timeouts: Ensure OS limits (like file descriptors, ephemeral port ranges if many connections outbound) are sufficient. In Java, if you reuse one client with a few connections, it’s fine. But if you had many microservices each making new connections rapidly, you could run into port exhaustion – again, that’s why connection reuse is critical.
- Bandwidth: For large values or high QPS, watch your NIC’s bandwidth. For example, if each value is 100KB and you do 10k gets/sec, that’s ~1GB/sec of data – 8 Gbps, nearing a 10Gbps NIC’s capacity. Either compress values or consider scaling out (multiple servers).
- Multiget vs Single-get: Batching gets as mentioned reduces network round trips, which is usually good. But if multi-get responses become very large, it can cause server to spend more time assembling a huge response. There’s a balance. Typically fetching 10–100 keys in a batch is fine. If you fetch 1000 keys in one request, you might saturate the network pipe for that one request’s response, causing others to wait.
Memory and Item Management Tuning: Memcached has a few tunables:
- Slab Growth Factor & Preallocation: You can adjust the slab size growth factor (
-f
option) if your item sizes vary widely. E.g., if you cache mostly similarly-sized items, you can reduce wasted space by adjusting chunk sizes. Usually default 1.25 factor is fine. If you see a lot of memory in one slab class free but not usable by another, Memcached has a feature called slab automover (-o modern
or-o slab_reassign
in newer versions) which can move empty pages from one slab class to another to handle changing workloads. - Max Item Size: via
-I
. If your workload needs larger items, set it at start. - LRU tuning: Modern memcached splits LRU into HOT, WARM, COLD and you can tweak how quickly items move between them (
-o lru_crawler
and related options). For most, it’s not needed to tweak, but know it’s there.
Scaling Horizontally (Adding Nodes): Memcached is inherently designed to scale out by adding more servers (each server uses its own memory and doesn’t sync with others). To scale:
- Manual Sharding via Client: The typical approach is simply to add another Memcached instance and update the client’s server list. If using consistent hashing, this will redistribute keys more gracefully; if using modulo hashing, it’ll cause a full reshuffle. When you add nodes, you increase total cache size and also spread the request load across more servers.
- Auto-Discovery: In dynamic environments, you might integrate with a service registry so clients know current memcached nodes. AWS ElastiCache for Memcached, for instance, provides a configuration endpoint that clients can query to get the list of node endpoints in the cluster, and some clients (like the AWS ElastiCache client library) can do auto-discovery of nodes. Otherwise, in Kubernetes, you might maintain a headless service and have clients resolve DNS for pods (though memcached clients typically expect a static list at startup).
- Scaling Reads: You can also scale by adding more clients (i.e., microservice instances). Memcached can handle many concurrent clients. If one memcached node becomes a bottleneck (CPU or network), adding another node and re-balancing keys is the solution (since there’s no internal replication, adding nodes directly spreads load).
- Failover Handling: If a memcached server goes down, clients must handle it. Many clients will detect a failed node (no response) and temporarily remove it from the hashing pool, so requests automatically go to the remaining nodes. This means keys that were on the failed node are effectively “misses” until that node comes back (or a new node is added in its place). In the meantime, those keys will be recomputed from DB (cache miss penalty). This is typically acceptable (eventually that node recovering brings cache back). The key point is that Memcached is not strongly fault-tolerant: a node loss = loss of that portion of the cache. It’s a cache, so that’s usually okay. If it’s not okay for your use case, you’d need to introduce replication (e.g., have two memcached clusters and write to both, or use a different tech).
- Redundancy Options: Some advanced setups use a concept of replicating cache to mitigate node loss. For example, Facebook’s mcrouter (a memcached proxy) can be configured to route each write to two memcached pools (primary and secondary) in different failure domains; reads come from primary unless it’s down then from secondary. This gives an HA cache at cost of double memory. There’s also a Memcached fork “Replicated Memcached” and others, but none became mainstream. In Java, spymemcached does have a “failure mode” where if a node is down it doesn’t rehash those keys to others (to maintain consistency when it returns). But typically using consistent hashing, a down node’s keys are just effectively moved to the next nodes (depending on how client handles it).
- Connection Pool scaling: If you have extremely high concurrency needs from a single client to a single memcached node, sometimes increasing parallel connections can help. XMemcached for instance could use say 4 connections to each server, which allows 4 parallel pipelines, potentially utilizing multi-core on server better. But don’t blindly increase – measure if saturation occurs (like if one connection’s throughput is maxed out).
High QPS Considerations: At very high operations per second (e.g., 200k+ ops/s on a cluster), even small inefficiencies matter:
- Use binary protocol if your client supports and it’s not deprecated in your environment – it has slightly less overhead and supports “quiet” commands (pipelining without replies for sets).
- Use
noreply
on writes if you don’t need the acknowledgment (saves network time). Both text and binary have no-reply modes. For example, if you’re doing a bulk of 100 set operations and you don’t care to check each result, you can issue them withnoreply
to avoid waiting for the “STORED” responses. Be careful: if a set fails (like out of memory), you wouldn’t know. - Keep keys and values as small as reasonable (trim unnecessary data, maybe store computed fields rather than whole objects if you never need all fields).
- If using multiple threads in your app to call Memcached, share the Memcached client instance (most clients are thread-safe and handle the sync internally). Creating one client per thread can multiply connections and overhead without benefit.
Monitoring Tools: To optimize, you need visibility. You can use:
memcached -vv
(very verbose logging) on a test instance to see what operations are coming.stats
outputs – could periodically log hit ratio.- Tools like mctop (Memcached Top) or the
stats latency
command (if enabled) can show microsecond-level timing of operations. - For Java, measure the time around cache calls. If most gets are sub-millisecond, great. If you see some calls taking longer, investigate (maybe GC pauses in your app, or network hiccups).
Example Calculation: If your service has 1000 requests per second and each request triggers 5 cache lookups on average, that’s 5000 gets/sec. If your hit rate is 90%, then 4500 of those get data from cache (fast), 500 go to DB. Suppose each DB query takes 50ms, and each cache get takes 0.5ms. The effective latency due to caching is much lower. If the cache wasn’t there, all 5000 would hit DB – which likely cannot handle that or would be much slower. So monitoring how caching improves things is often done by looking at reduced DB load and faster response times.
Consistent Hashing in Practice: If you use SpyMemcached, enabling Ketama (consistent hashing) is done like:
ConnectionFactoryBuilder builder = new ConnectionFactoryBuilder();
builder.setLocatorType(ConnectionFactoryBuilder.Locator.CONSISTENT);
MemcachedClient client = new MemcachedClient(builder.build(), AddrUtil.getAddresses("..."));
This ensures minimal cache disruption on scale-out. XMemcached uses consistent by default, but if you wanted the old mod behavior for some reason, you could change the HashAlgorithm.
Takeaway: Memcached can handle massive scale if used correctly – Facebook in 2008 had 800+ Memcached servers doing billions of lookups per second. They had to optimize at kernel and client level, but the core idea of distributed caching scaled. Most likely, your microservices architecture will use at most a handful to dozens of nodes. With careful client usage and consistent hashing, scaling out is linear and straightforward.
6. Advanced Usage Patterns
Here we cover some advanced topics and patterns: making Memcached highly available, using it in a multi-datacenter/hybrid environment, and securing it.
High Availability & Replication: Out-of-the-box, Memcached does not replicate data across nodes – each piece of data lives on exactly one server (determined by the hash of its key). If that server goes down, all its cached data is lost (though the data can still be reloaded from the source database as needed). For many scenarios, this is acceptable because caches are designed to be refreshable. However, if you need high availability of the cache layer (to avoid a cache miss storm on node failure), there are a few approaches:
- Client-side Redundant Writes: The client could write each item to two different Memcached servers (e.g., primary and secondary). On read, it tries primary first and if that fails, tries secondary. This way, if one server is down, hopefully the other has the value. This doubles the memory cost and write traffic, and is not commonly built-in to clients (it requires custom logic or a proxy). Facebook’s McDipper and some custom solutions have done similar things. It adds complexity in keeping those in sync (if one fails to write, etc.). In practice, most don’t do this except at extreme scale with custom proxies.
- Memcached Proxies (Mcrouter, Twemproxy): Facebook released Mcrouter, a memcached protocol router. It can be deployed between application and Memcached servers, and it can manage replication of keys and failover. For example, you can configure pools in Mcrouter so that each write goes to two memcached pools (maybe in different data centers), and reads come from one. Mcrouter will handle one pool failing by routing reads to the backup. This provides a transparent HA at cost of more infra. Twitter open-sourced Twemproxy (nutcracker), which can multiplex connections to memcached and also handle server pools (though Twemproxy doesn’t do replication; it’s more of a sharding proxy with connection pooling benefits).
- Persistent Backing Store: Some systems use Memcached as a front, but have a persistent cache behind. E.g., use Memcached for quick access, but also asynchronously write the data to a disk-based cache or DB. If memcached cluster goes down or gets cold, the persistent cache can quickly reload it. One example is using Redis (which has persistence) as a secondary cache – but then you might just use Redis for caching in the first place. Another example is Couchbase Server, which was originally “Memcached + disk + clustering”. Couchbase can be seen as a persistent, replicated memcached (with a Memcached-compatible API in earlier versions).
- Multi-AZ Deployments: On cloud, if you want HA, you could run memcached nodes in multiple availability zones. If one AZ goes down, you still have others. Your clients would experience those nodes as gone (lots of misses) but at least some cache remains on others. Because memcached doesn’t automatically refill data, you may still see a DB surge for the missing part.
It’s important to design your system to tolerate a cache node outage: ensure the database can handle temporary load or you have fallback caches. Many large deployments deliberately over-provision database or have circuit breakers on traffic when a cache is lost to prevent meltdown.
Distributed Caching Patterns: Aside from the typical single-tier cache, you can have multiple layers:
- L1/L2 Caches: It’s common to have a local in-process cache (L1) for extremely hot data, with Memcached as L2. For example, a microservice might cache some items in a Guava or Caffeine cache (with very short TTLs or small size) to avoid even the network call to Memcached for repeatedly accessed items in a tight loop. If not found in L1, then it goes to Memcached (L2). This is an optimization that can reduce latency by microseconds and offload some reads from memcached. The trade-off is complexity and the need to handle coherency (if one instance’s L1 has stale data after an update that came through L2, etc.). Typically, you’d design L1 as just a short-lived cache that doesn’t try to be strongly consistent – stale for a few seconds is okay.
- Hierarchical Caches: In some systems (like large CDNs or multi-level caching), Memcached might be used in a hierarchy. For instance, each application server might have a small local memcached, and there is a larger central memcached cluster as well. The app checks local memcached (fast, same host), if miss, checks central memcached, if miss, goes to DB. This is not common in microservices (since typically one memcached layer is enough), but can exist if optimizing for network hops. Memcached itself doesn’t have a built-in hierarchy management – you would manually implement that lookup sequence.
- Hybrid Cache (Distributed + Local): Similar to L1/L2, but sometimes called hybrid when combining different technologies (e.g., use Memcached for distributed cache of DB queries, and use browser caching or edge caching for final delivery). For microservices, “hybrid” could also mean using Memcached alongside Redis or others: e.g., use Memcached for transient cache, Redis for things like distributed locks or more complex cached structures.
- CDN and Memcached: If your microservice deals with content (like images), you might use a CDN (content delivery network) as an outer cache and Memcached as an inner cache for generated pages or API responses. They complement each other.
Security Concerns:
- DDoS Amplification: Memcached made news in 2018 for being used in one of the largest DDoS attacks recorded. Attackers exploited Memcached’s UDP protocol – sending a tiny UDP packet with a fake source IP and a special command that elicited a huge response from Memcached, thereby amplifying traffic to the victim. The lesson: Never expose Memcached directly to the internet. Always run it in a private network or behind firewalls. If using UDP and not needed, disable UDP (
-U 0
to turn it off). Modern Memcached versions have UDP off by default in some distros. Also, if running on a public cloud VM, ensure security groups block UDP on 11211 if not needed. - Authentication: Vanilla Memcached (text protocol) has no authentication – if you can connect, you can issue commands. The binary protocol introduced SASL authentication (with usernames and passwords) which is often used in managed services (e.g., MemCachier, Elasticache if configured with auth). If you run Memcached yourself and need auth, you either put it in a secure network or use SASL by running memcached with
-S
for SASL enabled (requires libsasl and setting up a saslpasswd file). Memcached’s meta protocol currently doesn’t support auth, so SASL is only with binary (which is one reason binary is still used in cloud services). If using a service like Azure Cache for Redis (which doesn’t support Memcached) or others, you might instead just ensure only your services can reach the cache. In microservices on Kubernetes, for example, you’d have no external Service for memcached, only internal. - Encryption: Memcached communication by default is unencrypted. Within a data center, this is usually fine. But if you have any cache traffic crossing data centers or untrusted networks, you should encrypt it. Memcached added TLS support in version 1.5.13+. You can start memcached with TLS mode (provide certs, keys, and use
-Z
). Alternatively, run it behind a stunnel or VPN. Some cloud cache services offer TLS endpoints. For Java, you’d need a client that supports TLS (some clients might not natively, but you could perhaps tunnel or use stunnel locally). Generally, keep cache traffic internal and secure at network layer. - Multi-Tenancy and Data Separation: If multiple applications share one Memcached cluster (not usually recommended unless they are related), one app could potentially read another’s data if keys are guessable. There’s no built-in namespace separation or ACL per key. So security by design: treat the whole Memcached cluster as one security domain. If separation is needed, run separate memcached instances or use different key prefixes with caution.
- Memory DoS: An attacker or bug could try to fill the cache with junk keys (cache pollution). Memcached has no item-level permission or quota. If a client can connect and issue commands, it could evict all your useful data by spamming different keys until the cache is full. This again is why access control is important – only allow trusted clients. Also, consistent hashing somewhat limits blast radius if an attacker doesn’t know all the node addresses (but security by obscurity isn’t reliable).
Distributed and Hybrid Caching Patterns:
- One interesting pattern is cache warming and backup: when you deploy a new instance of a microservice, it might initially have an empty local cache (if it has one). Some systems will proactively pre-fetch certain keys from Memcached or DB on startup (warming). Similarly, if you know a Memcached node will be restarted (planned maintenance), an approach is to dump its keys (though Memcached doesn’t have an easy bulk export; you’d need to iterate keys which is not trivial. Some folks use
stats cachedump
for that, but it’s not meant for production use). - Hybrid Memory Storage: Memcached now has a feature called Extstore (since 1.6) that allows using SSD as overflow storage (kind of a L2 within memcached process). Large items can spill to SSD. This blurs the line between pure memory cache and persistent cache. It’s advanced usage: if configured, memcached will keep keys and small metadata in RAM but values on SSD for cold items, effectively increasing cache size cheaply. This might be interesting for hybrid memory/disk caching if you have very large data that doesn’t all fit in RAM. However, using extstore means your cache isn’t purely in-memory for those items, with performance accordingly reduced (though still faster than going to a remote DB likely).
- Geographically Distributed Caching: If you have microservices across multiple data centers that each have their own local cache needs, typically you’d run separate Memcached clusters in each region (keeping caching local to each). If you try a global cache (with some nodes in one DC and some in another, using consistent hashing across them), your latency will be as bad as the farthest node and you introduce cross-DC traffic – usually not desired. Instead, treat caches as per-region and let each region’s cache warm up on its own data usage. If needed, you could asynchronously copy some cache data across regions (not common).
- Caching & Eventual Consistency: In microservices, sometimes the source of truth data might itself be eventually consistent (like data coming from another service via events). Caching in front of that needs careful thinking. For example, if Service A caches data from Service B, and B’s updates propagate via events, A might have stale data until it receives event to invalidate cache. Embrace that slight staleness window or design an invalidation strategy that covers eventual updates (maybe with slight delays or periodic consistency checks).
- Combining with Other caches: Memcached vs Redis is often considered. You might actually use both: Memcached for simple high-speed caching of objects, and Redis for more complex structures (sorted sets, lists, etc.) or for cases needing persistence/replication. For instance, a microservice could use Redis to manage a rate limiter or queue, and Memcached to cache database objects. They serve different roles. The choice could also be organizational – maybe Memcached is managed by one team and Redis by another. There’s no problem using them in tandem as long as each is used for what it’s best at.
Security Recap: Always run memcached in trusted networks, consider enabling SASL/TLS if on untrusted networks, and monitor for unusual usage patterns (like sudden spike in traffic which could hint at abuse).
In cloud deployments, leverage security groups (only allow the microservices’ subnets to talk to the cache). In Kubernetes, you might only allow access via an internal service (no external LB). If multi-tenant, likely separate caches per tenant.
To close advanced usage: Memcached remains a straightforward tool – it doesn’t solve consistency or replication for you, but that simplicity is what gives it speed. You often handle advanced patterns at the application level or with additional software (like proxies or sidecar processes). For many microservices, the advanced needs may not arise until you reach very large scale or high reliability requirements; at that point, one evaluates if Memcached is still the right fit or if a more feature-rich distributed cache is warranted.
7. Memcached in Modern Architectures
Memcached’s role in modern architectures (cloud, containers, etc.) is sometimes questioned in comparison to newer alternatives like Redis. Here we compare Memcached with Redis and discuss deployment in Docker/Kubernetes and usage of cloud-managed services.
Memcached vs Redis (and others): Memcached and Redis are both in-memory key-value stores but with different design philosophies:
- Data Structures: Memcached deals with opaque blobs identified by keys. Redis offers a variety of data structures (strings, hashes, lists, sets, sorted sets, streams, etc.) and related commands. If your use case is simply caching objects by key (no need for set operations or pub/sub, etc.), Memcached’s simplicity is sufficient. If you need those additional features (say caching not just a user object but also needing to increment a counter in a sorted leaderboard), Redis might serve both purposes in one system.
- Persistence: Memcached is volatile – a restart means all data gone. Redis can be configured to persist to disk (RDB snapshots or AOF log) and can replicate to slaves for durability. Thus, if you need an always-on cache or a data store that can survive reboots, Redis is a better choice. Memcached is meant to sit in front of a durable store, not be one.
- Scalability Model: Memcached is scaled by clients distributing to multiple nodes (no internal clustering in the product itself; you handle at client level). Redis (in recent versions) supports clustering natively – you can run a Redis Cluster where data is partitioned among nodes and the cluster can rebalance and handle failovers automatically (with replicas). That said, Redis Cluster also introduces some complexity and overhead. With Memcached, scaling is very transparent and linear but requires either static configuration or an external mechanism to manage node lists (like a service discovery).
- Performance and Memory Efficiency: Memcached is highly optimized for simple get/set at massive throughput. It’s multi-threaded, so on a multi-core machine it can use all cores. Redis, until version 6, was essentially single-threaded for execution (it now has I/O threading and some parallelism, but still one main event loop for commands). This means Memcached can often outperform Redis for purely storing and retrieving small objects in parallel. Memcached also has lower memory overhead per key, because it just stores the raw data plus a small header. Redis has more overhead (especially if you store a lot of small keys, the metadata and object pointers can add up). Some benchmarks have shown Memcached can be more memory-efficient for plain key-value (one test noted memcached using less memory to store the same data because Redis has to maintain its data structures and dictionaries). However, if your caching includes needing atomic operations or complex queries (e.g. incrementing a hash field or taking intersections of sets), Redis will shine, whereas with Memcached you’d be doing that logic in your app or not at all.
- Eviction Policies: Memcached uses LRU (global or per slab class) only. Redis has multiple eviction policies (allkeys-LRU, volatile-LRU, LFU, random eviction, etc.) and can be configured to never evict (if memory limit reached, it will error on writes). If you need more control over eviction (like using an LFU to keep frequently used items, not just recently used), Redis offers that.
- Partitioning Consistency: In Memcached, if one node goes down, those keys are just gone (clients hash to the others). In Redis Cluster, if a node goes down, if there’s a replica, it can failover and still serve those keys (no data loss if persisted). This makes Redis cluster a bit more resilient at the cost of complexity. But also in Redis cluster, if you lost a shard without replica, you can’t get those keys (similar situation). So for pure cache usage, both are similar in that losing one means losing the cached data, except Redis could optionally persist so the data can come back on restart, effectively making it a bit more than a cache (a database).
- Community and Ecosystem: Redis has a huge ecosystem, many more clients, and an active developer community adding features. Memcached is mature and somewhat “done” (changes are infrequent, mostly maintenance and occasional feature like meta commands). This means if you need something fancy, chances are someone did it with Redis. Memcached’s maturity means it’s extremely stable and proven, but not a lot of new features will come.
Which to choose? For a pure caching layer that you want simplest and fastest, Memcached is great. It’s also very easy to set up and requires almost no configuration. Many web stacks continue to use Memcached for page caching, query caching, etc., where the access pattern is straightforward. Redis might be chosen if you want your cache to double as a shared data store or you need rich operations on the cached data. Some use Redis as a “swiss army knife” – not just for caching but for queues, locks, leaderboards, etc., thereby consolidating infrastructure. However, Redis’s extra features come with the need to manage persistence and replication if you want reliability, which can be more maintenance.
In microservices, one could use Memcached for caching database reads, and use Redis for things like distributed locks or pub/sub between services. Using both is not uncommon, as they complement each other.
Docker and Containerization: Memcached is very easy to containerize. There’s an official Docker image memcached
which you can use, e.g., docker run -d -p 11211:11211 memcached:latest
starts a memcached server with default 64MB memory. For production use in Docker/Kubernetes:
- You’ll want to adjust the memory limit (
memcached -m <MB>
) to match what you allocate to the container. If you run in Kubernetes with a memory limit of, say, 1Gi, also start memcached with-m 1024
to use that. - Ensure using
--restart
policies or in Kubernetes a Deployment/StatefulSet so that if a memcached container dies it restarts. - Storage is not needed (unless using extstore). Memcached stores everything in RAM, so no persistent volume is required.
- On Kubernetes, consider using a StatefulSet for memcached if you want stable hostnames for each node (e.g., memcached-0, memcached-1, etc.) which you can then feed into your clients. A Headless Service can be used for discovery (so DNS returns all pod IPs). Without a headless service, you might just manually configure each memcached service (like separate service per pod).
- There are also helm charts and operators (e.g., memcached-operator) to deploy a Memcached cluster easily.
- Keep in mind that scaling memcached up/down in Kubernetes might be tricky if your clients don’t auto-discover. One approach is to tie the memcached cluster size to your deployment config and update the client configuration in your microservices via env vars or config maps when it changes. Alternatively, some dynamic client or proxy is used.
Cloud-Managed Memcached: Major clouds have managed offerings:
- AWS ElastiCache (Memcached mode): You can create an ElastiCache cluster in Memcached mode. This essentially spins up memcached nodes for you. It provides configuration endpoint for auto-discovery and some integration with CloudWatch for metrics. You can scale up/down the cluster (though scaling down evicts data of removed nodes). It does not provide persistence or replication for Memcached (unlike Redis mode which does). It’s basically a convenient way to get memcached instances without managing EC2 yourself.
- Google Cloud Memorystore for Memcached: GCP launched Memorystore for Memcached (in addition to their Redis). It’s fully managed – you specify number of nodes, memory, etc., and GCP handles deploying them (likely on GCE under the hood). It also supports auto-discovery via a GCP API so that clients can fetch the node list.
- Azure: Azure doesn’t have a native Memcached service; they focus on Redis (Azure Cache for Redis). On Azure you’d run memcached on VMs or containers if needed.
- Others: Some third-party or PaaS providers (like MemCachier, ElasticCache by Instaclustr, etc.) offer hosted Memcached.
Using a managed service can simplify operations, especially if you don’t want to worry about container management or host failures. But ensure your client supports whatever integration needed (for AWS, you either use their SDK or a client that handles auto-discovery or treat it as static if cluster is fixed). Memcached being stateless (no persistence) means that scaling or replacing nodes in managed service will flush those nodes’ data; some managed solutions might try to do it seamlessly (like adding nodes then removing old ones gradually).
Kubernetes and Microservices deployment:
- If each microservice has very distinct caching needs, sometimes teams run separate memcached clusters per service. This provides isolation (one service’s heavy cache usage doesn’t evict data from another’s cache) at the cost of some duplicate data perhaps. It might also simplify key naming (no need for prefixes per service). On k8s, that could mean each service’s helm chart or manifest includes a memcached deployment just for it. This is okay for moderate scale, though at some point you might consolidate to save memory if there’s a lot of overlap or under-utilization.
- Service Mesh Consideration: If using a service mesh (like Istio), memcached is typically just treated as an external service. You might not want the mesh sidecar on memcached itself (it would add overhead to each operation). Likely you’d disable sidecar injection on memcached pods or not route cache traffic through the mesh if low latency is key.
- Autoscaling: memcached doesn’t auto-shard itself, so if you want to autoscale the number of pods based on load, your clients need to become aware. This often isn’t done; instead you scale manually or with a StatefulSet (scaling manually or via script). If using something like Mcrouter, autoscaling can be easier since the router can handle an expanding pool.
Memcached in Serverless or Edge contexts: As serverless computing (like AWS Lambda) grows, can we use Memcached? Yes, but with caution:
- A Lambda function could connect to a memcached endpoint (like ElastiCache). Cold start might have to initialize a new connection (which is a bit slow for a short function). If the lambda is warm and re-used, it can keep the connection in memory for subsequent invocations. There’s no persistent process to hold the connection pool, which can lead to a high connection churn if many lambdas spin up.
- One might prefer Redis in such context, but memcached is fine if usage is infrequent or low-latency requirements aren’t extreme (the network calls from lambda in same VPC are typically fine).
- For edge computing (like Cloudflare Workers), you likely wouldn’t be calling Memcached directly (they might use KV storage or in-memory at edge). But if you had your own edge servers, memcached can run on each edge node to cache upstream results.
Combining with CDNs/Front proxies: E.g., in a microservices app serving web content, a request might go: User -> CDN -> Web Service -> Memcached (for HTML fragment) / DB. The CDN handles static, Memcached handles dynamic caching. They stack to provide multi-layer caching.
Modern hardware advantages: Memcached benefits from lots of RAM and fast network. On modern hardware with NVMe and 100Gb networks, memcached can drive immense throughput (with extstore, using NVMe for extended cache). There are also efforts like Netflix EVCache which is a memcached-based solution on Java (actually they use a custom memcached client and multiple replicas). Netflix open-sourced EVCache which basically writes to multiple memcached in different AZs for HA.
Trend: Redis popularity vs Memcached: It’s worth noting that Redis has arguably overtaken Memcached in popularity for new projects, because of its versatility. However, Memcached is still very widely used in existing large systems (Facebook, Twitter, Wikipedia all continue to use it heavily alongside other solutions) and continues to be supported. Some managed services (like AWS) still see lots of memcached usage where users only need a cache and prefer the simplicity or lower cost (Memcached is somewhat cheaper on AWS if you only need memory store without replication).
Interoperability: If you consider switching from Memcached to Redis or vice versa in future, note:
- They have different protocol and clients, so you’d need code changes or a bridging layer.
- Some caching libraries abstract both behind interfaces (e.g., Spring Cache could let you swap underlying provider with config changes).
- MemCachier’s blog in 2025 might compare current differences, but the core points remain what we discussed.
Microservice Consideration – Database vs Cache vs Messaging: Memcached should not be abused as a database. If your data truly needs persistence or relational access, use a DB. Use Memcached to supplement and speed up, not to be the single source of truth. Also, Memcached is not a message broker or queue – don’t use it to pass messages between services (use a proper message queue or streaming platform). It’s strictly for caching ephemeral data.
Example of Cloud Deployment: Suppose you deploy an online store microservices app to AWS. You might use ElastiCache Memcached with 5 nodes, 20 GB each, for a total of 100 GB cache. Your product service, cart service, and user service all connect to this cluster (with distinct key namespaces). You set your clients to use Auto Discovery (provided by AWS SDK) so if you resize cluster AWS updates the config endpoint and clients pick up changes. You monitor CloudWatch metrics – if the miss rate or evictions climb, you scale out to 7 nodes. If one node fails, ElastiCache will detect and replace it, and clients will auto-update (some cache data lost, but your DB scales enough to handle repopulating those keys gradually).
Another scenario – Kubernetes: You have a Memcached StatefulSet with 3 replicas. You configure your client with the DNS of those (e.g., memcached-0.memcache.mynamespace.svc.cluster.local
, etc.). If you want to scale to 4, you update the StatefulSet replicas and update the client configuration in a ConfigMap that your service uses (and rolling restart the service to take new config). Alternatively, you run one memcached per host (DaemonSet) caching local things – but that’s unusual unless each node runs an entire slice of the app’s data (still need global coordination if any node can serve any request, so not typical).
In sum, Memcached remains highly relevant in modern cloud-native applications when used for what it’s best at: a fast, volatile distributed cache. It pairs well with microservices that require quick read access to common data and can tolerate eventual consistency. It’s simpler than many modern data stores, which also means fewer moving parts (no elections, no write-ahead logs, etc., to manage).
8. Real-World Case Studies
To ground our understanding, let’s look at how some large-scale systems have utilized and optimized Memcached. These cases illustrate best practices and clever tricks, as well as pitfalls encountered.
Facebook: Facebook is often synonymous with Memcached at scale. Around 2008, Facebook was likely the world’s largest Memcached user. They deployed thousands of Memcached servers (over 800 servers with 28 TB total cache by 2008) to cache the results of database queries and generated pages. This cache tier sat between the web servers (hundreds of Apache/PHP servers) and databases. They credit Memcached with allowing them to serve billions of reads with acceptable database load. However, operating at that scale led to unique challenges:
- Memory Optimization: With hundreds of thousands of client connections, they found Memcached’s per-connection overhead significant. Facebook engineers modified Memcached to use a shared buffer pool for connections, saving gigabytes of RAM that were getting wasted in socket buffers.
- UDP for Multiget: To reduce TCP overhead on multi-get, Facebook moved to using UDP for fetches. By doing so, they avoided TCP connection management overhead for the extremely high request rates. They had to patch Linux kernel to handle UDP better (reducing lock contention).
- Mcrouter: As the cluster grew, managing connections from every web server to every memcached became an issue (hundreds of thousands of connections). Facebook introduced mcrouter, a Memcached protocol proxy. Web servers would connect to a local mcrouter, which would then manage a pool of persistent connections to memcached servers, multiplexing requests. This reduced the number of connections and allowed better routing logic (like replication, failover). Mcrouter also implemented consistent hashing and more advanced routing (e.g., region-based routing and pooling).
- Multiget Hole: Facebook observed an issue where using huge multiget requests and adding servers didn’t increase capacity linearly. They called this the “multiget hole”. Essentially, if each user profile view does a multiget of N keys, adding a new memcached server doesn’t reduce the number of memcached operations; it just spreads them. The number of keys per request goes down, but total requests across cluster remain the same (and CPU was the bottleneck). The fix was to adjust application behavior (limit multiget size) and to ensure memcached instances weren’t CPU-saturated (scale out earlier or use more cores).
- Consistency and Dogpile: Facebook dealt with the dogpile effect (many cache misses at once) by using a variety of techniques, including having caches populate slightly before expiration and using memcache’s add operation to allow only one process to set a “lock” key. They also used short TTLs plus external triggers (Facebook’s data is very dynamic, so they often explicitly invalidated cache on writes, using their TAO framework or triggers from the database).
- TAO & Memcached: Eventually, Facebook developed TAO, a cache specifically for the social graph. But they still use Memcached for general purpose caching (like serving ads, etc.). Over the years, they’ve published that they heavily optimized network stack and even looked at alternative caching systems (e.g., integrating flash storage as memcache backend which they termed “McDipper”).
Twitter: Twitter also used Memcached heavily, though they eventually forked it as Twemcache. They had a variety of use cases: caching tweets, timelines, etc. Some insights:
- Twitter’s Twemcache fork made memcached single-threaded again but improved how memory is managed. In Twemcache, they opted for a different eviction strategy (slab-based eviction). Instead of evicting one item at a time, Twemcache can evict an entire slab (all items in the least recently used slab class) to reallocate memory to another class. This helps when the working set’s size distribution shifts drastically – it mitigates the stuck memory in slab classes issue. Essentially, Twemcache trades possibly evicting more items at once for the ability to reassign memory.
- Twitter found that many of their workloads were not read-heavy but had a fair amount of writes (like counting events, etc.), which influenced their caching design. They also encountered issues with consistent hashing when adding/removing nodes frequently, and at one point they used a precomputed jump hash (like jump consistent hashing) to minimize movement.
- Eventually, Twitter needed even more from caching and started Pelikan, a suite of caching servers (including a Memcached-compatible one and a Redis-compatible one) written in Rust/C that could leverage multi-core better and have richer features. But Twemcache was a stable workhorse for a long time.
- Twitter’s usage was also integrated with FlockDB and their timeline service, often caching rendered timelines. They’ve mentioned how a cache miss on a hot timeline could cause significant DB load, so they implemented locking or refresh ahead.
Wikipedia (Wikimedia): Wikimedia has a large memcached deployment for Wikipedia and related sites. MediaWiki (the software behind Wikipedia) uses memcached for several purposes: object caching (recent article text, metadata), parser caching (to cache rendered HTML of wiki pages), and session data. They have multiple data centers with memcached and they use a consistent hashing setup. Some specifics:
- They often run memcached on the same servers as application servers (to use spare RAM), but logically treat them as a distributed cache cluster.
- They use mcrouter in front of memcached to manage clusters across multiple data centers. For example, reads might go to local memcached, if not found, mcrouter can be configured to try a remote memcached cluster.
- Because Wikipedia is read-heavy and the content changes relatively slowly compared to read frequency, caching is extremely effective. They report very high hit rates, which is crucial since it allows most page views to not hit the backend storage or re-render.
- For invalidation, when an edit is made to a page, they invalidate that page’s cache (using a versioning scheme or explicit deletes). They also have to handle things like purging derived caches (like if a template is edited, all pages using it should be invalidated – they queue such invalidations).
- Wikimedia has published that they sometimes use a “spillover” for less frequently used data to secondary storage to keep hot cache small. But primarily, memcached is their front line for caching everything from API responses to user preferences.
YouTube and Reddit: (Based on general knowledge since specific details are scarce in citations)
- YouTube: Likely used memcached in early years for caching video metadata, user sessions, etc., given it was part of Google later, they might have moved to other internal solutions (like their own in-memory store). But memcached is known to have been part of scaling LAMP stack sites like YouTube pre-Google.
- Reddit: Known to use memcached for things like caching rendered comment trees, and to handle their heavy read traffic on popular threads. Reddit as of a few years ago had a Python stack that heavily relied on memcached to reduce database hits (especially as certain pages would be extremely high traffic after a viral post).
Instagram: (Not mentioned in prompt but notable) – Instagram wrote about using Django’s caching (which can use memcached) to speed up their API responses and how they partition caches by region of data, etc.
Best Practices Learned from These Cases:
- Design your application to tolerate cache failures (graceful degradation).
- Keep cache keys and usage patterns relatively simple; complexity in caching (like caching extremely complex inter-dependent data without proper invalidation) can lead to consistency bugs.
- Instrumentation: All these companies built good dashboards for cache metrics to know when something is off (e.g., a sudden drop in hit rate could mean an unexpected cache flush or an increase in invalidations).
- Prevention of stampede: Use techniques such as request coalescing (only one DB fetch for N requests) at scale. Facebook, for example, in some cases allowed slightly stale data rather than all processes trying to refresh at once.
- Monitoring cache efficiency: They often calculate the efficacy of the cache: e.g., measure the average DB query time saved by a cache hit, to justify how much cache to use for what. Facebook found some queries were so cheap that caching them was not worth it – identifying those allows focusing cache on the expensive operations.
- Capacity planning: At large scale, they model how many QPS per server and how much network. For instance, Facebook moved to 10GigE early for memcached servers to handle throughput.
- Custom Clients/Optimizations: Many wrote custom memcached clients in optimized languages (C/C++). For example, Facebook’s client is in C++ for performance; Twitter used Finagle (Scala) for asynchronous memcached calls.
Troubleshooting Tips from Real World:
- If you see memcached high CPU, check for any large commands (like someone using
stats cachedump
which is expensive, or very large multigets). - If you experience a stampede (DB overload) after a memcached outage, consider adding backpressure – e.g., let the service degrade (return errors or static info) for some requests instead of all hitting DB at once.
- Avoid overusing memcached as a queue (some tried using incr/decr and gets to implement a lock or a job queue – not ideal).
- Use longer TTLs for data that doesn’t change often, to maximize cache utility; use short TTL or none for rapidly changing data to reduce staleness (or consider not caching highly volatile data at all).
- Memcached can sometimes evict something before TTL if LRU and full. If you see that, it means your TTL is too long relative to cache size or your cache is undersized for the working set.
Example of an issue and solution: Suppose at WikiCorp, they found that every day at 00:00 UTC a bunch of keys expired together (because they were set with TTL that ended at midnight). This caused a spike of DB queries. They solved it by adding a random stagger to TTL for those sets, so they don’t all expire at once (cache avalanche solution).
Another scenario: At ShopNow (fictional), they had a sale and a particular product page got extremely popular (hot key). The memcached node holding it became maxed out. Their short-term mitigation was to manually copy that key’s data to another memcached node and modify the code to fetch from one of two keys (simple form of read replication). A more general solution they adopted later was to implement client-side request spreading: when a key is too hot, the client library detects it and temporarily treats the key as if it were multiple keys (like key#1, key#2) to spread load – a very advanced trick and rarely needed unless you’re at extreme traffic on one or two keys (mostly an edge case like celebrity account on a social media).
Outcome of these Case Studies: All these services successfully used Memcached to scale reads by orders of magnitude. They also contributed improvements back: e.g., Facebook’s patches improved memcached for everyone (things like better slab automation, LRU Crawler, etc., were influenced by large-scale needs). The key lesson is that Memcached, while simple, can be bent and tuned to handle very large workloads. The strategies (consistent hashing, replication via client, avoiding stampedes) that we discuss are often drawn from these experiences.
9. Future Directions
Memcached is a mature technology, but the landscape of distributed caching and memory-centric storage continues to evolve. In this final section, we consider the future of Memcached and caching in microservices:
- Protocol and Feature Evolution: The introduction of the meta text protocol in 2019 suggests Memcached is adapting to needs such as cache stampede prevention and more efficient operations. Future versions of Memcached may expand on this. For instance, meta commands allow fetching an item with flags that say “do not move to head of LRU” or “fetch even if expired” (to implement stale-while-revalidate), etc. We can expect Memcached to possibly deprecate the older ASCII commands eventually in favor of meta (since it’s more flexible and binary is already deprecated). If you write custom clients, targeting meta protocol is the forward-looking approach.
- Integration with Emerging Tech: As microservices often run on orchestration platforms, Memcached might see better integration hooks. For example, an operator that automatically reconfigures clients on scaling events. Or Memcached with built-in support for service discovery (perhaps via a plugin system). Also, consider service meshes – maybe future Memcached could expose a gRPC interface or similar (though that might reduce performance compared to raw memcached protocol, it could simplify some deployments).
- High Availability Enhancements: While Memcached itself likely won’t implement replication (as that’s explicitly out of scope historically), there might be higher-level projects to provide an HA caching layer. For example, Netflix EVCache as mentioned – we might see more open-source solutions or cloud offerings that wrap Memcached with a replication layer (outside of the memcached process).
- Hybrid Memory (RAM + SSD): The extstore feature in Memcached is an interesting area. It effectively allows Memcached to use SSD as extension of RAM cache, acting like a cache-tiered system (like memory as L1, SSD as L2). This blurs the line with systems like Aerospike or Couchbase (which use SSD and RAM). If extstore proves useful, we may see improvements to it (performance tuning, easier management) and adoption in scenarios where working set is larger than RAM but still latency sensitive. This could keep Memcached relevant as memory per dollar trade-offs shift (SSD is cheaper per GB than DRAM).
- Security Improvements: Given the emphasis on security now, Memcached might formally include easier configuration for TLS and authentication out-of-the-box (so it can be more easily used in zero-trust environments). Right now enabling TLS requires compiling with SSL support and config options. A future where memcached can be deployed with a config file including TLS settings (instead of only CLI flags) might come.
- Cache Consistency and Coordination: There’s academic and open-source interest in making caches more intelligent. For instance, protocols to invalidate or update distributed caches on data changes (like a publish-subscribe of invalidation events). Memcached doesn’t do this inherently, but a future extension or sidecar that listens to change events (perhaps from database binlogs or triggers) and invalidates keys could be part of an ecosystem solution. E.g., a MySQL plugin that sends invalidation commands to memcached on table updates.
- Integration with Kubernetes Operators: We might see official Memcached operators that handle cluster membership automatically. Already, projects exist in that direction. This will make Memcached more cloud-native, handling scaling and failover more seamlessly for users.
- Competing Technologies: The future of Memcached also depends on competition. Redis continues to add features (Redis module system adds JSON storage, search, etc.). In-memory SQL caches and NewSQL databases sometimes remove the need for an external cache. There’s also the rise of distributed in-memory data grids (Hazelcast, Apache Ignite) which provide caching with strong consistency, query capability, etc. Memcached’s niche is simplicity and raw speed – it may continue to thrive by focusing on that and being easy to deploy everywhere. But if, say, Redis addresses its own minor weaknesses (like making Redis cluster easier, or reducing overhead), some might consolidate on Redis for simplicity (one system vs two). However, Memcached still often outperforms Redis for straightforward caching at lower resource usage, and large companies still invest in it (Twitter’s Pelikan has a memcached-compatible service, meaning they still see value in that interface).
- Memory Efficiency Research: There’s ongoing research on better cache eviction algorithms (like TinyLFU, etc. which are shown to improve hit rates). Memcached’s LRU could potentially be augmented or replaced by ARC or LFU if it proved significantly better. Already, an option for LRU tuned with frequency (LFU) was introduced in Redis. Memcached devs might consider such changes if community demands.
- AI and Caching: It’s buzzwordy, but one could imagine using machine learning to predict which items to cache or evict. Not likely something to be inside memcached, but at the application layer, one might use ML to decide TTLs or prefetch certain data into cache before it’s requested (based on pattern recognition).
- Edge Caching and CDNs: As edge computing grows, memcached could find a place as a lightweight cache on edge nodes. It’s simpler than running a full Redis and could cache API responses or user data at the network edge. With the rise of microservice architectures that include edge services (like AWS Lambda@Edge or Cloudflare Workers), maybe a memcached-like API could exist at edge to persist data between invocations (some platforms have limitations there though).
- Continued Relevance in Microservices: In the microservice world of 2025 and beyond, there is a movement toward unified data layers (some talk of distributed caches that are multi-region, etc.). Memcached, being stateless, fits well with containerized deployment and ephemeral compute. It’s likely to remain a fundamental building block, possibly behind the scenes even if developers are interfacing through higher-level abstractions. For example, a service mesh or a platform might provide a “cache service” to developers and under the hood it’s a memcached cluster.
In summary, Memcached’s future will likely be about maintaining its strengths (speed, simplicity) while adapting to new deployment paradigms (cloud-native, secured environments) and possibly inter-operating with other layers (like being managed by proxies or orchestrators to provide features it doesn’t natively have). The core protocol and usage has not drastically changed in its nearly two decades of existence – which is a testament to the soundness of its design for the problem it solves. We expect Memcached to continue to be a reliable workhorse for distributed caching, especially in scenarios where the utmost performance and straightforward key-value semantics are needed.
Sources:
- Wikipedia: Memcached overview and history
- Memcached GitHub Wiki: Memory allocation (slabs) and LRU behavior
- Memcached Documentation: ASCII protocol commands and CAS explanation
- MemCachier Blog: Java client usage and recommendation (SpyMemcached vs XMemcached)
- Facebook Engineering (2008): Scaling Memcached at Facebook – insights on UDP, connection pooling
- HighScalability: Facebook’s Memcached multiget hole – multiget and scaling observations
- Twitter Open Source: Twemcache differences – slab eviction vs object eviction
- Alibaba Cloud Tech: Memcached in Microservices (architect’s view)
- ByteByteGo (Alex Xu) 2023: Caching pitfalls (stampede, penetration)
- Siemens Blog: Memcached Memory Model – slab and eviction example