All topics
Database · Learning hub

Memcached notes for developers

Master Memcached with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Memcached quizMore Database notes
Memcached

Memcached Essentials

Memcached Essentials Memcached is a distributed, in-memory key-value store built for one job: caching the result of expensive work (a database query, an API cal

Memcached Essentials

Memcached is a distributed, in-memory key-value store built for one job: caching the result of expensive work (a database query, an API call, a rendered template) so the next request can skip it. It's deliberately minimal — no persistence, no data structures beyond flat byte strings, no built-in replication. That simplicity is the point: memcached is fast and predictable because it doesn't try to be a database. When people compare it to Redis, the short version is that Redis is a data-structure server that happens to be great at caching, while memcached is a caching layer that refuses to be anything else.

The Key-Value Model

Everything in memcached is a key mapped to an opaque blob of bytes (a string, a serialized object, JSON) with an optional expiration (TTL). There are no lists, hashes, or sorted sets like Redis has — if you need structure, you serialize it yourself before storing and deserialize after reading. Keys are limited to 250 bytes and values to 1MB by default (configurable with `-I`), which is a deliberate constraint: memcached is for small, hot, frequently-read objects, not for storing large blobs. The core operations are `set`, `get`, `add` (set only if the key doesn't exist), `replace` (set only if it does), `delete`, and the atomic counters `incr`/`decr`.

# Start memcached: 64MB memory, port 11211, 4 worker threads
memcached -m 64 -p 11211 -t 4 -d

# Connect directly over telnet/nc to speak the text protocol
nc localhost 11211

# set <key> <flags> <exptime_seconds> <bytes>
set user:42 0 300 13
Hello, World!
STORED

get user:42
VALUE user:42 0 13
Hello, World!
END

# add fails if the key already exists — useful for locks and dedup
add user:42 0 300 5
other
NOT_STORED

# atomic counter — no read-modify-write race
set views:page1 0 0 1
0
STORED
incr views:page1 1
1
incr views:page1 1
2

delete user:42
DELETED

stats
# shows cmd_get, cmd_set, get_hits, get_misses, evictions, bytes, curr_items, uptime...

Using It From Application Code

In practice you talk to memcached through a client library, not raw telnet. The cache-aside pattern — check the cache, fall back to the source of truth on a miss, then populate the cache — is by far the most common usage, and it's worth internalizing because it's the same shape regardless of language or client.

import json
from pymemcache.client.base import Client

client = Client(('localhost', 11211))

def get_user_profile(user_id):
    cache_key = f'user_profile:{user_id}'
    cached = client.get(cache_key)
    if cached is not None:
        return json.loads(cached)

    # Cache miss — fall back to the database
    profile = db.query(
        'SELECT id, name, email, plan FROM users WHERE id = %s', (user_id,)
    )
    if profile is None:
        return None

    # Populate the cache with a TTL so stale data eventually expires on its own
    client.set(cache_key, json.dumps(profile), expire=300)
    return profile

def invalidate_user_profile(user_id):
    # Explicit invalidation on write — don't wait for the TTL to expire
    client.delete(f'user_profile:{user_id}')

# get_multi batches several lookups into one round trip — much cheaper
# than N separate get() calls when rendering a page with many cached pieces
keys = [f'user_profile:{uid}' for uid in [1, 2, 3, 4, 5]]
results = client.get_multi(keys)

Eviction, LRU, and Slab Allocation

Memcached never grows past the memory limit you give it (`-m`). Once it's full, it doesn't error on writes — it evicts. Internally, memory is divided into slab classes, each holding fixed-size chunks (e.g., 96 bytes, 120 bytes, 152 bytes...) so that storing and freeing values doesn't fragment memory the way a general-purpose allocator would. Each slab class keeps its own LRU (least-recently-used) list, and when a class runs out of room, the least-recently-used item in that specific class is evicted to make room for the new one — not necessarily the least-recently-used item overall. This is why two workloads with very different value sizes can have very different eviction behavior even under the same total memory limit.

  • Watch `evictions` in `stats` — a rising eviction count under normal traffic means your working set doesn't fit in the memory you've allocated, and cache hit rate is silently dropping.

  • Slab class fragmentation is real: a value that's 1 byte over a slab boundary gets rounded up to the next class, wasting the difference. If your keys have wildly varying value sizes, tune `-f` (growth factor) to get finer-grained slab classes.

Scaling Out: Client-Side Consistent Hashing

Memcached servers don't talk to each other — there's no cluster, no gossip protocol, no replication between nodes. Scaling to multiple servers is entirely the client's job: the client library hashes each key to decide which server in the pool owns it, and all clients need to agree on that mapping so they hit the same server for the same key. Naive modulo hashing (`hash(key) % N`) is a trap — adding or removing one server reshuffles nearly every key's target server, causing a massive, synchronized cache miss storm. Consistent hashing solves this by mapping both servers and keys onto a hash ring; when a server is added or removed, only the keys adjacent to that server on the ring move, leaving the rest of the mapping intact.

from pymemcache.client.hash import HashClient

# HashClient uses consistent hashing across the pool automatically —
# losing/adding one node only remaps ~1/N of the keyspace, not all of it
client = HashClient([
    ('cache1.internal', 11211),
    ('cache2.internal', 11211),
    ('cache3.internal', 11211),
], use_pooling=True)

client.set('session:abc123', 'user_data_blob', expire=1800)
value = client.get('session:abc123')  # always routes to the same node for this key

Memcached vs Redis: Why Choose the Simpler Tool

Redis has largely eaten memcached's market for new projects because it does everything memcached does plus persistence, replication, pub/sub, and rich data structures — so why does memcached still get chosen? Multithreading is the biggest technical reason: memcached's architecture shards work across multiple worker threads natively, while a single Redis instance is fundamentally single-threaded for command execution (Redis 6+ added I/O threading, but command processing itself is still one thread). For a pure, high-throughput, GET/SET-only cache with many CPU cores available, memcached can push more ops/sec per instance with a simpler operational model — no persistence to configure, no eviction policy to tune, no risk of someone accidentally using it as a database because it fundamentally can't be.

Gotchas & Practical Tips

  • There is zero persistence. A restart, a crash, or an OOM kill wipes everything instantly. Never treat memcached as a source of truth or a durable session store without an application-level fallback.

  • The thundering herd problem: when a hot key expires, many concurrent requests can all miss the cache at once and hammer the database simultaneously trying to repopulate it. Mitigate with jittered TTLs, a short-lived "recompute lock" key, or probabilistic early expiration.

  • There is no built-in authentication or encryption in the classic text protocol — memcached should never be exposed directly to the public internet. Bind it to a private network/localhost and firewall port 11211.

  • Prefer `get_multi`/batched calls over loops of individual `get` calls — each round trip has network overhead, and batching is often the single biggest win available without changing your cache strategy at all.

  • `incr`/`decr` only operate on values already stored as decimal strings — you can't `incr` a key that doesn't exist or holds non-numeric data; initialize it with `set` first.

  • A TTL of 0 means "never expire" (until evicted for space), not "expire immediately" — a common off-by-mistake for developers coming from other caching APIs.

Keep your Memcached knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever