Memcached #
Memcached is a very simple and very fast in-memory cache. It’s deliberately minimal: it only supports one data type (strings/bytes), no persistence, no built-in replication, no complex data structures. Its simplicity is its strength — per-operation overhead is lower than Redis, and its multi-threaded architecture uses multi-core CPUs more effectively. Memcached scales horizontally by adding servers, and clients use consistent hashing to determine which server stores each key. In Rust, the memcache crate provides synchronous access, and async-memcached covers async needs. This article covers all Memcached operations, idiomatic caching patterns, and when to choose Memcached over Redis.
Memcached vs Redis — Choosing the Right One #
Before diving into code, it’s important to understand when Memcached is the better choice over Redis:
flowchart TD
Q{Caching needs?}
Q --> A["Only need to cache\nsimple strings/bytes\nMaximum throughput\nOptimal multi-core CPU"]
Q --> B["Need data structures\n(Hash, List, Set)\nPersistence\nPub/Sub\nLua scripts"]
Q --> C["Distributed cluster\nwith linear\nhorizontal scalability"]
A --> MC["Memcached\n✓ Simpler\n✓ Native multi-threaded\n✓ Lower overhead"]
B --> RD["Redis\n✓ Richer features\n✓ Optional persistence\n✓ Complex atomic operations"]
C --> MC2["Memcached\n✓ Consistent hashing\n✓ Linear scaling\n✓ No inter-node coordination"]| Aspect | Memcached | Redis |
|---|---|---|
| Data types | Only strings/bytes | 10+ types (String, Hash, List, Set, ZSet, etc.) |
| Persistence | No | Optional (AOF/RDB) |
| Threading | Multi-threaded | Single-threaded (I/O) |
| Clustering | Client-side sharding | Built-in Cluster mode |
| Replication | Not native | Built-in |
| Atomic operations | CAS, INCR/DECR | Many (MULTI, Lua, etc.) |
| Memory | More efficient per item | More overhead |
| Complexity | Very low | Moderate |
| When | Simple caching, high throughput | Full features, persistence |
Installation #
[dependencies]
# Synchronous driver
memcache = "0.17"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
# Or for async
# async-memcached = "0.4"
Thememcachecrate is synchronous. For async applications, run Memcached operations insidetokio::task::spawn_blockingso the event loop isn’t blocked, or use the newer (but still developing)async-memcachedcrate.
Basic Connections #
use memcache::Client;
fn buat_client(url: &str) -> Result<Client, memcache::MemcacheError> {
// Connect to one server
Client::connect(url)
}
fn buat_client_multi(urls: &[&str]) -> Result<Client, memcache::MemcacheError> {
// Connect to several servers — the client performs consistent hashing
Client::connect(urls.to_vec())
}
fn main() -> Result<(), memcache::MemcacheError> {
// Format: memcache://host:port or memcache://host:port?timeout=5&tcp_nodelay=true
let client = buat_client("memcache://localhost:11211")?;
// Ping to verify
client.version()?;
println!("Connected to Memcached");
// Cluster: keys are distributed across servers based on consistent hashing
let cluster = buat_client_multi(&[
"memcache://server1:11211",
"memcache://server2:11211",
"memcache://server3:11211",
])?;
Ok(())
}
Basic Operations #
SET — Storing Values #
use memcache::Client;
fn contoh_set(client: &Client) -> Result<(), memcache::MemcacheError> {
// SET with expiry in seconds (0 = never expires)
client.set("nama", "Budi Santoso", 3600)?; // expires in 1 hour
client.set("counter", 42u64, 0)?; // never expires
client.set("aktif", true, 600)?; // expires in 10 minutes
// SET with raw bytes
let data = b"binary data";
client.set("raw", data.as_ref(), 300)?;
// SET with a struct serialized to JSON
let config = serde_json::json!({
"host": "localhost",
"port": 8080,
"debug": true
});
client.set("config", config.to_string().as_str(), 3600)?;
println!("SET succeeded");
Ok(())
}
GET — Retrieving Values #
fn contoh_get(client: &Client) -> Result<(), memcache::MemcacheError> {
// GET a string
let nama: Option<String> = client.get("nama")?;
match nama {
Some(n) => println!("Name: {}", n),
None => println!("Key not found or already expired"),
}
// GET with a different type
let counter: Option<u64> = client.get("counter")?;
println!("Counter: {:?}", counter);
// GET JSON and parse into a struct
if let Some(json_str): Option<String> = client.get("config")? {
let config: serde_json::Value = serde_json::from_str(&json_str)
.unwrap_or_default();
println!("Port: {}", config["port"]);
}
// GET many keys at once
let banyak: std::collections::HashMap<String, String> = client.gets(
&["nama", "config", "kunci_tidak_ada"]
)?;
println!("Multi GET: {} keys found", banyak.len());
for (k, v) in &banyak {
println!(" {}: {}...", k, &v[..v.len().min(30)]);
}
Ok(())
}
ADD, REPLACE, and DELETE #
fn contoh_lainnya(client: &Client) -> Result<(), memcache::MemcacheError> {
// ADD — set ONLY if the key doesn't exist (like SET NX in Redis)
let berhasil = client.add("lock:resource", "owner-1", 30)?;
println!("ADD succeeded: {}", berhasil); // true on success
// ADD again on the same key → fails (already exists)
let gagal = client.add("lock:resource", "owner-2", 30)?;
println!("Second ADD (should fail): {}", gagal); // false
// REPLACE — set ONLY if the key already exists
client.set("existing", "nilai-awal", 300)?;
let berhasil_replace = client.replace("existing", "nilai-baru", 300)?;
println!("REPLACE succeeded: {}", berhasil_replace); // true
let gagal_replace = client.replace("tidak-ada", "nilai", 300)?;
println!("REPLACE on missing key (should fail): {}", gagal_replace); // false
// DELETE — remove a key
client.delete("nama")?;
println!("Key 'nama' deleted");
// DELETE a missing key — no error
client.delete("kunci-tidak-ada")?;
// FLUSH ALL — remove all keys (be careful in production!)
// client.flush()?; // uncomment to flush everything
Ok(())
}
CAS — Check-and-Set (Optimistic Locking) #
CAS enables atomic updates: get a value together with a unique token (CAS token), make modifications, then set it back — but only if the token is still valid (nothing changed the value in between):
fn contoh_cas(client: &Client) -> Result<(), memcache::MemcacheError> {
// Store the initial value
client.set("saldo", "1000000", 3600)?;
// GETS — get the value together with a CAS token
let hasil: Option<(Vec<u8>, u64)> = client.gets_cas("saldo")?;
if let Some((data, cas_token)) = hasil {
let saldo_str = String::from_utf8_lossy(&data);
let saldo: u64 = saldo_str.parse().unwrap_or(0);
println!("Current balance: {}, CAS token: {}", saldo, cas_token);
let saldo_baru = saldo - 50_000; // simulate a debit
// CAS — set only if the token is still valid
// If another transaction changed the balance between this GETS and CAS,
// the operation fails and must be retried
let berhasil = client.cas("saldo", &saldo_baru.to_string(), 3600, cas_token)?;
if berhasil {
println!("Balance updated: {}", saldo_baru);
} else {
println!("CAS failed — balance changed by another process, retry");
}
}
Ok(())
}
// CAS with a retry loop — the common optimistic locking pattern
fn perbarui_dengan_cas(
client: &Client,
kunci: &str,
transform: impl Fn(u64) -> u64,
maks_retry: u32,
) -> Result<u64, String> {
for percobaan in 0..maks_retry {
let hasil: Option<(Vec<u8>, u64)> = client.gets_cas(kunci)
.map_err(|e| e.to_string())?;
match hasil {
None => return Err(format!("Key '{}' not found", kunci)),
Some((data, cas_token)) => {
let nilai: u64 = String::from_utf8_lossy(&data)
.parse()
.unwrap_or(0);
let nilai_baru = transform(nilai);
let berhasil = client.cas(kunci, &nilai_baru.to_string(), 3600, cas_token)
.map_err(|e| e.to_string())?;
if berhasil {
return Ok(nilai_baru);
}
println!("Attempt {} failed, retrying...", percobaan + 1);
}
}
}
Err(format!("Failed after {} attempts", maks_retry))
}
Atomic Increment and Decrement #
fn contoh_incr_decr(client: &Client) -> Result<(), memcache::MemcacheError> {
// Set the initial value as a numeric string
client.set("halaman_views", "0", 0)?;
client.set("slot_tersedia", "100", 0)?;
// INCREMENT — add atomically
let views: u64 = client.increment("halaman_views", 1)?;
println!("Views: {}", views); // 1
let views_multi: u64 = client.increment("halaman_views", 10)?;
println!("Views after +10: {}", views_multi); // 11
// DECREMENT — subtract atomically
let slot: u64 = client.decrement("slot_tersedia", 1)?;
println!("Available slots: {}", slot); // 99
// DECREMENT can't go negative — stops at 0
client.set("kecil", "2", 0)?;
let _ = client.decrement("kecil", 10)?; // becomes 0, not -8
let nilai: Option<u64> = client.get("kecil")?;
println!("Minimum value is 0: {:?}", nilai);
Ok(())
}
Caching Patterns with Async via spawn_blocking #
use std::sync::Arc;
use memcache::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
struct DaftarProduk {
pub items: Vec<String>,
pub total: u64,
pub halaman: u32,
}
// Wrap Client in Arc for shared ownership
type McClient = Arc<Client>;
async fn ambil_daftar_produk(
mc: McClient,
halaman: u32,
) -> Result<DaftarProduk, Box<dyn std::error::Error + Send + Sync>> {
let kunci = format!("produk:halaman:{}", halaman);
// Run Memcached operations in the thread pool so the async runtime isn't blocked
let mc_clone = Arc::clone(&mc);
let kunci_clone = kunci.clone();
let cached = tokio::task::spawn_blocking(move || {
mc_clone.get::<String>(&kunci_clone)
})
.await??;
if let Some(json_str) = cached {
println!("Cache HIT: page {}", halaman);
let daftar: DaftarProduk = serde_json::from_str(&json_str)?;
return Ok(daftar);
}
// Cache MISS — get from the database
println!("Cache MISS: page {}", halaman);
let daftar = DaftarProduk {
items: (1..=10).map(|i| format!("Produk {}", i + (halaman - 1) * 10)).collect(),
total: 100,
halaman,
};
// Store in the cache
let json_str = serde_json::to_string(&daftar)?;
let mc_store = Arc::clone(&mc);
tokio::task::spawn_blocking(move || {
mc_store.set(&kunci, json_str.as_str(), 300)
})
.await??;
Ok(daftar)
}
// Generic wrapper for the cache-aside pattern
async fn dengan_cache<T, F, Fut>(
mc: McClient,
kunci: &str,
ttl: u32,
fetch: F,
) -> Result<T, Box<dyn std::error::Error + Send + Sync>>
where
T: serde::Serialize + for<'de> serde::Deserialize<'de> + Send + 'static,
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, Box<dyn std::error::Error + Send + Sync>>>,
{
let mc_get = Arc::clone(&mc);
let kunci_str = kunci.to_string();
// Check the cache
let cached: Option<String> = tokio::task::spawn_blocking(move || {
mc_get.get::<String>(&kunci_str)
})
.await??;
if let Some(json) = cached {
if let Ok(nilai) = serde_json::from_str::<T>(&json) {
return Ok(nilai);
}
}
// Fetch from the source
let nilai = fetch().await?;
// Store in the cache
let json = serde_json::to_string(&nilai)?;
let mc_set = Arc::clone(&mc);
let kunci_set = kunci.to_string();
tokio::task::spawn_blocking(move || {
mc_set.set(&kunci_set, json.as_str(), ttl)
})
.await??;
Ok(nilai)
}
Distributed Caching with Consistent Hashing #
Memcached distributes keys across several servers automatically on the client side:
fn contoh_distribusi(client: &Client) -> Result<(), memcache::MemcacheError> {
// The client automatically distributes keys to different servers
// based on consistent hashing of the key name
// These keys may be stored on different servers
for i in 0..20 {
let kunci = format!("item:{}", i);
let nilai = format!("nilai-{}", i);
client.set(&kunci, nilai.as_str(), 300)?;
}
// Read them all — the client knows which server to ask for each key
for i in 0..20 {
let kunci = format!("item:{}", i);
let nilai: Option<String> = client.get(&kunci)?;
println!("{}: {:?}", kunci, nilai);
}
// Server stats — info per server
let stats = client.stats()?;
for (server, stat_map) in stats {
println!("Server: {}", server);
if let Some(bytes) = stat_map.get("bytes") {
println!(" Memory used: {} bytes", bytes);
}
if let Some(curr_items) = stat_map.get("curr_items") {
println!(" Current items: {}", curr_items);
}
if let Some(hits) = stat_map.get("get_hits") {
println!(" Cache hits: {}", hits);
}
if let Some(misses) = stat_map.get("get_misses") {
println!(" Cache misses: {}", misses);
}
}
Ok(())
}
Cache Invalidation Strategies #
fn strategi_invalidasi(client: &Client) -> Result<(), memcache::MemcacheError> {
// 1. TTL-based: auto-expires after N seconds
client.set("data:ttl", "nilai", 300)?;
// 2. Explicit delete: remove when data changes
client.delete("data:produk:1001")?;
// 3. Namespace versioning: bump the version for mass invalidation
// Instead of deleting thousands of keys one by one, bump the namespace version
client.set("ns:produk:v", "2", 0)?; // new version = 2
// New keys use the latest version
let versi: Option<String> = client.get("ns:produk:v")?;
let v = versi.as_deref().unwrap_or("1");
let kunci_produk = format!("produk:v{}:1001", v);
client.set(&kunci_produk, "data-produk-baru", 300)?;
// Keys with the old version will not be found (namespace switched)
// This approach avoids thundering herds and cache stampedes
// 4. Tag-based invalidation — store a list of keys per tag
// (Memcached has no native support, implemented manually via special keys)
client.set("tags:kategori:elektronik", "key1,key2,key3", 0)?;
Ok(())
}
When to Choose Memcached vs Redis #
Use Memcached if:
✓ Only need simple value caching (strings/bytes/JSON)
✓ Highest throughput and lowest latency are the priority
✓ Linear horizontal scaling without inter-node coordination
✓ No persistence needed — the cache can be lost on restart
✓ The team is familiar with the simple key-value model
Use Redis if:
✓ Need data structures (Hash, List, Sorted Set for leaderboards)
✓ Need persistence (data must not be lost on restart)
✓ Need Pub/Sub for real-time notifications
✓ Reliable distributed locks with Lua scripts
✓ Sliding-window rate limiting (Sorted Sets)
✓ A session store that needs querying by field
✓ Automatic replication and failover (Redis Sentinel/Cluster)
Summary #
- Memcached only supports strings/bytes — no hashes, lists, or sorted sets like Redis. For data structures, serialize to JSON and store as a string.
- CAS for optimistic locking —
gets_casgets the value and token,casupdates only if the token is still valid. This is Memcached’s way of preventing race conditions without blocking.addfor simple locks —addfails if the key already exists, equivalent toSET NXin Redis. Combine with a TTL for a distributed lock without CAS.- Atomic
increment/decrement— can’t go negative (stops at 0). Use for counters that must never be negative.- Wrap in
spawn_blockingfor async — thememcachecrate is synchronous; run it in the thread pool so the tokio runtime isn’t blocked.- Consistent hashing on the client side — no coordination between Memcached servers. When servers are added or removed, the percentage of keys needing rehashing is minimal.
- Namespace versioning for mass invalidation — bump the namespace version instead of deleting thousands of keys one by one. Prevents cache stampedes during large-scale invalidation.
- Monitor with
client.stats()— watch hit rates, miss rates, and per-server memory usage for proper tuning.- Choose Redis for full features, Memcached for simplicity and throughput — when in doubt, start with Redis because it’s more flexible; switch to Memcached only with clear evidence of a bottleneck.