Memcached #

Memcached is a distributed in-memory caching system designed with an extreme simplicity principle — one data type (string/bytes), one main operation (get/set/delete), no persistence, no built-in replication. Precisely because of its simplicity, Memcached is very fast and easy to scale horizontally: add a new server, distribute keys using consistent hashing, done. Memcached is the right choice when you need lightweight pure caching without the overhead of extra features. Understanding its limitations — key/value size limits, no persistence, no complex data structures — is key to choosing when Memcached is the right tool.

Installation #

pip install pymemcache

To run Memcached locally:

# Docker
docker run -d --name memcached -p 11211:11211 memcached:latest

# With a 256MB memory limit
docker run -d --name memcached -p 11211:11211 memcached:latest memcached -m 256 -t 4

Creating a Connection #

pymemcache provides several client types: Client for a single server, PooledClient for multi-threading, and HashClient for distribution across several servers.

import json
import os
from pymemcache.client.base import Client, PooledClient
from pymemcache.client.hash import HashClient

MEMCACHED_HOST = os.getenv("MEMCACHED_HOST", "localhost")
MEMCACHED_PORT = int(os.getenv("MEMCACHED_PORT", "11211"))

# ANTI-PATTERN: a plain Client in a multi-threaded environment
client = Client(("localhost", 11211))  # ✗ -- not thread-safe without a pool

# CORRECT: PooledClient for web applications (thread-safe with a connection pool)
client = PooledClient(
    (MEMCACHED_HOST, MEMCACHED_PORT),
    max_pool_size=10,      # number of connections in the pool
    pool_idle_timeout=60,  # close idle connections after 60 seconds
    connect_timeout=5,     # timeout when connecting
    timeout=3,             # timeout during operations
    serde=None             # we'll use a custom serde below
)

# Test the connection
try:
    client.set("ping", "pong", expire=5)
    assert client.get("ping") == b"pong"
    print("Memcached connection successful.")
except Exception as e:
    print(f"Connection failed: {e}")

JSON Serializer #

By default, pymemcache stores and returns bytes. To work with dicts and lists, implement a custom serializer.

import json
from pymemcache.client.base import PooledClient

class JSONSerde:
    """JSON serializer/deserializer for pymemcache."""

    def serialize(self, key, value):
        if isinstance(value, (dict, list)):
            return json.dumps(value, ensure_ascii=False).encode("utf-8"), 2
        if isinstance(value, str):
            return value.encode("utf-8"), 1
        return value, 0

    def deserialize(self, key, value, flags):
        if flags == 2:
            return json.loads(value.decode("utf-8"))
        if flags == 1:
            return value.decode("utf-8")
        return value

client = PooledClient(
    (MEMCACHED_HOST, MEMCACHED_PORT),
    max_pool_size=10,
    connect_timeout=5,
    timeout=3,
    serde=JSONSerde()    # enable the JSON serializer
)

# Now you can store and retrieve dicts transparently
client.set("pengguna:42", {"id": 42, "nama": "Budi", "email": "[email protected]"})
pengguna = client.get("pengguna:42")
print(type(pengguna))    # <class 'dict'>
print(pengguna["nama"])  # Budi

Basic Operations #

# Set with a TTL (expire in seconds)
client.set("kunci", "nilai", expire=3600)       # expire in 1 hour
client.set("token:abc", "user-42", expire=300)  # expire in 5 minutes
client.set("permanent", "data")                 # no TTL (until evicted)

# Get -- returns None if missing or expired
nilai = client.get("kunci")
print(nilai)   # "nilai" (if using JSONSerde)

# Delete
client.delete("kunci")

# add() -- set only if the key doesn't exist (atomic, like SET NX in Redis)
berhasil = client.add("lock:proses", "1", expire=30)
print("Set successful:", berhasil)   # True if new, False if it already exists

# replace() -- set only if the key already exists
berhasil = client.replace("kunci", "nilai_baru", expire=3600)

Cache-Aside Pattern #

from typing import Callable, Any

def get_atau_set(client, cache_key: str, fetch_fn: Callable, ttl_detik: int = 3600) -> Any:
    """
    Cache-Aside: check cache → hit? return; miss? fetch, store, return.
    """
    # Sanitize the key -- Memcached disallows spaces, max 250 characters
    cache_key = cache_key.replace(" ", "_")[:250]

    nilai = client.get(cache_key)
    if nilai is not None:
        print(f"Cache HIT: {cache_key}")
        return nilai

    print(f"Cache MISS: {cache_key}")
    data = fetch_fn()

    if data is not None:
        client.set(cache_key, data, expire=ttl_detik)

    return data

def ambil_produk_dari_db(produk_id: int) -> dict:
    return {"id": produk_id, "nama": "Laptop Gaming", "harga": 18500000}

produk = get_atau_set(
    client,
    cache_key=f"produk:{101}",
    fetch_fn=lambda: ambil_produk_dari_db(101),
    ttl_detik=1800
)

Batch Operations #

# Set many keys at once
client.set_many({
    "produk:101": {"id": 101, "nama": "Laptop Gaming",     "harga": 18500000},
    "produk:102": {"id": 102, "nama": "Samsung Galaxy S24", "harga": 15000000},
    "produk:103": {"id": 103, "nama": "Nike Air Max",       "harga": 1800000},
}, expire=1800)

# Get many keys at once -- more efficient than a get() loop
keys  = ["produk:101", "produk:102", "produk:103", "produk:999"]
hasil = client.get_many(keys)
# hasil is a dict {key: value} -- only the found keys

for key in keys:
    if key in hasil:
        print(f"HIT: {key}{hasil[key]['nama']}")
    else:
        print(f"MISS: {key}")

# Delete many keys at once
client.delete_many(["produk:101", "produk:102", "produk:103"])

Atomic Increment and Decrement #

# Page view counter
client.set("views:artikel:5", "0", expire=86400)  # the value must be string/bytes for incr

client.incr("views:artikel:5", 1)    # +1 (atomic)
client.incr("views:artikel:5", 10)   # +10
nilai = client.get("views:artikel:5")
print(f"Views: {nilai}")  # b'11'

# Simple rate limiting
def cek_rate_limit(client, user_id: str, maks: int = 100, window: int = 60) -> bool:
    key = f"rl:{user_id}"
    try:
        jumlah = client.incr(key, 1)
        if jumlah is None:
            # Key doesn't exist -- set with a TTL
            client.set(key, "1", expire=window)
            return True
        return int(jumlah) <= maks
    except Exception:
        return True   # fail open if Memcached errors

for i in range(5):
    diizinkan = cek_rate_limit(client, "user-42", maks=3, window=60)
    print(f"Request {i+1}: {'✓ allowed' if diizinkan else '✗ rejected'}")

HashClient — Multi-Server Distribution #

HashClient distributes keys across several Memcached servers using consistent hashing — the standard way to scale Memcached horizontally.

from pymemcache.client.hash import HashClient

# Server list from an environment variable
servers_env = os.getenv("MEMCACHED_SERVERS", "localhost:11211")
servers_raw = [s.split(":") for s in servers_env.split(",")]
servers     = [(host, int(port)) for host, port in servers_raw]

cluster = HashClient(
    servers,
    serde=JSONSerde(),
    connect_timeout=5,
    timeout=3,
    use_pooling=True,     # connection pooling per server
    max_pool_size=5,
    retry_attempts=2,     # try another server if it doesn't respond
    retry_timeout=0.1,
    dead_timeout=30       # mark a server dead for 30 seconds
)

# Usage is identical to PooledClient
cluster.set("produk:200", {"id": 200, "nama": "MacBook Pro"}, expire=3600)
print(cluster.get("produk:200"))
When adding or removing servers from a Memcached cluster, some keys get rehashed to different servers, causing mass cache misses. Add new servers gradually and do cache warming before removing old servers to mitigate the impact.

Namespace Versioning for Bulk Invalidation #

Memcached doesn’t support pattern matching like Redis’s KEYS produk:*. To invalidate a group of keys at once, use a version counter per namespace.

def get_namespace_version(client, namespace: str) -> int:
    versi = client.get(f"ns:{namespace}")
    if versi is None:
        client.set(f"ns:{namespace}", "1", expire=86400)
        return 1
    return int(versi)

def build_key(client, namespace: str, key: str) -> str:
    versi = get_namespace_version(client, namespace)
    return f"{namespace}:v{versi}:{key}"

def invalidasi_namespace(client, namespace: str) -> None:
    """Invalidate an entire group -- increment the version, all old keys become invalid."""
    client.incr(f"ns:{namespace}", 1)
    print(f"Namespace '{namespace}' invalidated.")

# Store with a namespace
key = build_key(client, "produk", "101")
client.set(key, {"id": 101, "nama": "Laptop"}, expire=3600)

# Retrieve with a namespace
nilai = client.get(build_key(client, "produk", "101"))
print(nilai)

# Invalidate all product caches at once (without deleting one by one)
invalidasi_namespace(client, "produk")
# All "produk:v1:..." keys will never be found again
# New keys automatically use "produk:v2:..."

Error Handling and Fallback #

from pymemcache.exceptions import MemcacheError

def set_cache_aman(client, key: str, nilai: Any, expire: int = 3600) -> bool:
    try:
        # Validate the size -- Memcached max 1MB per value
        serialized = json.dumps(nilai, ensure_ascii=False)
        if len(serialized.encode("utf-8")) > 900_000:
            print(f"Value too large to cache ({len(serialized)} bytes)")
            return False

        client.set(key, nilai, expire=expire)
        return True
    except MemcacheError as e:
        print(f"Memcached error: {e}")
        return False

def ambil_data_dengan_fallback(client, key: str, fetch_fn: Callable) -> Any:
    """Get from the cache, fall back to the source if the cache is down."""
    try:
        cached = client.get(key)
        if cached is not None:
            return cached
    except Exception:
        pass   # Memcached unavailable, go straight to the source

    data = fetch_fn()
    try:
        if data is not None:
            client.set(key, data, expire=3600)
    except Exception:
        pass   # Can't store the cache, but the data is still returned

    return data

Memcached vs Redis #

Choose Memcached when:
  ✓ You need simple, lightweight pure caching
  ✓ You want horizontal scaling with many servers (multi-threaded, efficient per core)
  ✓ The cached data is only simple strings/bytes
  ✓ A smaller memory footprint is a priority

Choose Redis when:
  ✓ You need data structures: Hash, List, Set, Sorted Set, Stream
  ✓ You need data persistence (RDB/AOF snapshots)
  ✓ You need Pub/Sub, Distributed Locks, or Lua scripting
  ✓ You need complex atomic operations (pipelines + WATCH)
  ✓ You want one tool for caching + messaging + queues

Memcached limitations to remember:
  ✗ Keys max 250 characters, no spaces allowed
  ✗ Values max 1MB per key
  ✗ No persistence -- data is lost on restart
  ✗ No built-in replication
  ✗ No pattern matching for bulk invalidation

Summary #

  • PooledClient not Client — always use it in multi-threaded environments; a plain Client isn’t thread-safe without pooling.
  • Custom JSONSerde — implement one so you can store and retrieve dicts/lists without manual serialize/deserialize every time.
  • add() for atomic set-if-not-exists — use it for simple distributed locks or preventing overwrites; safer than a non-atomic set() + get().
  • get_many() and set_many() — always use batch operations for multiple keys; one network round-trip for many keys.
  • HashClient for multi-server — distribute keys across several servers with consistent hashing; configure retry_attempts and dead_timeout for failure tolerance.
  • Namespace versioning — since there’s no pattern matching, use a version counter per namespace to efficiently invalidate a group of keys at once.
  • Validate size before storing — Memcached rejects values over 1MB; check the size before set() to avoid runtime errors.
  • Sanitize keys — keys can’t contain spaces or control characters; replace spaces and cap at 250 characters.
  • Fail open on errors — catch all exceptions and continue to the database; the cache is an optimization, not a single point of failure.
  • Choose Redis for more than caching — if you need data structures, persistence, or Pub/Sub, Redis is the right choice; Memcached excels only at simple pure key-value caching.

← Previous: Redis   Next: Django →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact