Skip to content

Memcached Hoja de referencia

High-performance distributed memory object caching system.

01

Getting Started

Connect & Basic Commands

Memcached uses a simple text protocol on port 11211. The set command takes flags (a 32-bit integer opaque to the server), exptime (expiration in seconds, 0 = no expiry), and byte size. Values are stored as raw bytes — the server never inspects or modifies them. memcached-tool ships with the server and is handy for quick stats.

memcached
# connect to a memcached server
telnet localhost 11211

# or use the memcached-tool (ships with the server)
memcached-tool localhost:11211 stats

# set a key (flags, exptime, bytes)
set mykey 0 3600 5
hello

# get a key
get mykey

# delete a key
delete mykey

Protocol Overview

Memcached supports both an ASCII text protocol (easy to use with telnet/netcat) and a binary protocol (more efficient, supports SASL authentication and atomic operations). The binary protocol uses a fixed 24-byte header and is preferred by most production clients. Both protocols operate on the same underlying key-value store.

memcached
# Two protocols available:
# 1) ASCII text protocol (human-readable, telnet-friendly)
# 2) Binary protocol (lower overhead, SASL auth, request pipelining)

# text protocol example
set foo 0 0 3
bar
# -> STORED

# binary protocol needs a client (e.g. python binary protocol):
#   0x80 (request) | opcode | keylen | extras | datatype | reserved
#   bodylen | opaque | cas | extras | key | value

# both share the same in-memory data store

# check server version (text)
version
# -> VERSION 1.6.24

Key Rules & Limits

Keys are limited to 250 bytes and cannot contain spaces or control characters in the text protocol. The default max value size is 1MB (raise with the -I flag, e.g. -I 4m). Keys are not copied — Memcached stores them inline, so longer keys consume more memory. Use short, structured keys like user:1001 rather than verbose descriptive names.

memcached
# Keys: max 250 bytes, no spaces or newlines (text protocol)
# Controls: max 1MB default value (configurable via -I)

# valid keys
set user:1001 0 0 5
alice
set product:sku-42 0 0 3
xyz

# INVALID keys (text protocol)
# set "with spaces" ...   -> spaces not allowed
# set mykey\n ...         -> newlines not allowed

# large value exceeds 1MB default
set bigkey 0 0 1048577
# -> SERVER_ERROR object too large for cache

# increase max item size at startup (-I in MB)
# memcached -m 256 -I 4m

Common Use Cases

Memcached is best for caching transient, derived data: database query results, sessions, rendered page fragments, rate-limit counters, and expensive computations. It is intentionally simple — no persistence, no replication, no complex data types. Treat it as a volatile LRU cache: never store data you cannot rebuild from a system of record.

memcached
# 1) Database query cache
set user:1001:profile 0 600 48
{"id":1001,"name":"Alice","email":"[email protected]"}

# 2) Session storage (with TTL)
set sess:abc123 0 1800 64
{"userId":1001,"loginAt":1700000000,"role":"admin"}

# 3) Rendered HTML fragment cache
set page:home:en 0 300 128
<html><body>Hello</body></html>

# 4) Rate-limit counters
incr ratelimit:ip:1.2.3.4 1

# 5) Computed result cache (expensive aggregation)
set report:daily:20240101 0 3600 256
{"visits":1234,"signups":56}

Architecture Overview

Memcached is a distributed cache made of independent, uncoordinated servers. There is no clustering, replication, or failover in the server itself — all distribution logic lives in the client (consistent hashing). Each node owns a disjoint slice of the keyspace; if a node dies, its data is lost and clients reroute. This keeps the server dead-simple and fast, at the cost of durability.

memcached
# Memcached is a pool of independent servers (no clustering built-in)
#
#  +---------+   +---------+   +---------+
#  | mc:11211|   | mc:11211|   | mc:11211|
#  +---------+   +---------+   +---------+
#        ^            ^             ^
#        |            |             |
#   +----+------------+-------------+----+
#   |   CLIENT (consistent hashing)     |
#   +------------------------------------+
#
# - Each server owns a disjoint slice of the keyspace
# - Sharding is done by the CLIENT (consistent hashing)
# - Servers do NOT talk to each other (no replication, no failover)
# - A server down = its keys are lost; clients reroute to the ring

# horizontal scale = add more servers to the ring
# vertical scale = bigger -m memory per server
02

Basic Commands (set / get / delete)

set — Store a Value

set unconditionally stores a value, overwriting any existing key. flags is a 32-bit integer the server stores but never interprets — clients use it to encode serialization format (e.g. 1 = JSON, 2 = compressed). exptime 0 means no expiration; a Unix timestamp > 30 days is treated as an absolute time. noreply skips the response for fire-and-forget writes (faster, but no error reporting).

memcached
# syntax: set <key> <flags> <exptime> <bytes> [noreply]\r\n<data>\r\n
# response: STORED | NOT_STORED

set greeting 0 0 5
hello
# -> STORED

# with flags (app-defined 32-bit int, e.g. mark as JSON=1)
set config 1 0 14
{"a":1,"b":2}
# -> STORED

# with expiration (3600 seconds) and noreply
set token 0 3600 8 noreply
s3cr3t00
# -> (no response)

# overwrite an existing key
set greeting 0 0 7
goodbye
# -> STORED

get — Retrieve Values

get retrieves one or more keys in a single round-trip — batching (multi-get) is critical for performance. Each returned VALUE line echoes the flags so the client knows how to deserialize. Missing keys are silently omitted (no error). The response terminates with END. Large multi-gets should be chunked to avoid blocking a single server thread.

memcached
# single key
get greeting
# VALUE greeting 0 7
# goodbye
# END

# multiple keys in one request (batched)
get user:1 user:2 user:3
# VALUE user:1 0 5
# alice
# VALUE user:3 0 3
# bob
# END   (user:2 missing -> simply omitted)

# response format: VALUE <key> <flags> <bytes>\r\n<data>\r\n ... END
# flags lets the client decode (e.g. decompress / parse JSON)

# non-existent key
get nope
# END   (no VALUE line at all)

gets — Retrieve with CAS Token

gets (get-extended) is identical to get but appends a cas_unique token to each VALUE line. This 64-bit integer changes on every write and is the foundation of Memcached's optimistic concurrency (CAS). Use gets + cas together to avoid lost updates when multiple clients modify the same key. The token is opaque — never assume its value or monotonicity across keys.

memcached
# gets returns a CAS unique identifier for optimistic concurrency
gets greeting
# VALUE greeting 0 7 12
# goodbye
# END
#                          ^ cas_unique = 12

# the cas_unique changes every time the key is written
set greeting 0 0 5
hello
gets greeting
# VALUE greeting 0 5 13
# hello
# END   (cas_unique is now 13)

# use cas_unique with the cas command for safe updates
cas greeting 0 0 5 13
world
# -> STORED

delete — Remove a Key

delete removes a single key, returning DELETED or NOT_FOUND. There is no wildcard or pattern deletion — Memcached has no key enumeration command at all, so bulk deletion must be tracked by the application (maintain a set of keys elsewhere or use namespaced flush patterns). Add noreply to skip the response. Deleted memory is returned to its slab and reused.

memcached
# delete a single key
delete greeting
# -> DELETED

# delete a non-existent key
delete nope
# -> NOT_FOUND

# with noreply (fire-and-forget)
delete stalekey 0 noreply
# -> (no response)

# delete does NOT support patterns or wildcards
# delete user:*   <- INVALID, only deletes a key literally named "user:*"

# to bulk-delete, list keys yourself then delete each
# (memcached has no KEYS/SCAN command — track keys in your app)

touch & gat — Update Expiration

touch (1.4.8+) updates a key's expiration without transferring the value — cheaper than get+set for session refresh. gat fetches the value and updates the TTL atomically in one round-trip, ideal for session reads that should extend their lifetime. gats also returns the CAS token. Use these instead of get+set to avoid a race where the value changes between operations.

memcached
# touch: change a key's expiration without fetching the value
touch session 3600
# -> TOUCHED

touch session 0       # make it never expire
# -> TOUCHED

# gat: get-and-touch — fetch value AND update expiration in one op
gat 3600 session
# VALUE session 0 64
# {...}
# END

# gats: get-and-touch with CAS token
gats 3600 session
# VALUE session 0 64 42
# {...}
# END

# touch a missing key
touch nope 60
# -> NOT_FOUND
03

Storage Commands (add / replace / append / prepend)

add — Store Only If Not Exists

add stores a value only if the key does NOT already exist, returning NOT_STORED otherwise. It is the canonical way to lazily initialize keys (counters, locks) without a race. Combined with incr/decr it forms a safe counter pattern: add to create at 0, then incr. Unlike Redis SETNX, add has no TTL shortcut — set exptime in the command.

memcached
# add fails if the key already exists
add newkey 0 0 5
hello
# -> STORED

add newkey 0 0 5
world
# -> NOT_STORED   (key already exists)

# common pattern: initialize a counter only once
add counter:daily 0 0 1
0
# -> STORED on first run, NOT_STORED after

# then use incr to bump it
incr counter:daily 1
# -> 1

replace — Store Only If Exists

replace stores a value only if the key ALREADY exists — the mirror of add. It prevents accidentally creating a cache entry from a stale update path. Returns NOT_STORED when the key is absent. Use it when a write should refresh existing cache but never populate a new entry (e.g. a background refresh that should not resurrect an evicted key).

memcached
# replace fails if the key does NOT exist
replace missing 0 0 5
hello
# -> NOT_STORED  (key doesn't exist)

set existing 0 0 3
foo
# -> STORED

replace existing 0 0 3
bar
# -> STORED   (key existed, value updated)

# useful for updating cached data only if it was already cached
# (don't accidentally create a cache entry from a stale write)
replace cached:user:1 0 600 64
{"name":"Alice"}

append — Append to Existing Value

append concatenates data to the end of an existing value, returning NOT_STORED if the key is absent. The flags and exptime arguments are accepted but ignored — the value keeps its original metadata. This is great for building log lines or buffers in place without read-modify-write. Beware: the resulting value must still fit under the 1MB item limit.

memcached
# append adds data to the END of an existing value
set logline 0 0 5
hello
# -> STORED

append logline 0 0 6
 world
# -> STORED

get logline
# VALUE logline 0 11
# hello world
# END

# append to a non-existent key
append nolog 0 0 3
abc
# -> NOT_STORED  (key must exist)

# flags and exptime are IGNORED on append/prepend (value keeps old metadata)

prepend — Prepend to Existing Value

prepend is the mirror of append — it concatenates data to the START of an existing value. Like append, it requires the key to exist and ignores flags/exptime. Useful for stacking headers/prefixes onto a buffered value. append and prepend are NOT atomic read-modify-write across clients in the sense that they don't return the result — but they are server-side atomic operations.

memcached
# prepend adds data to the START of an existing value
set greeting 0 0 5
world
# -> STORED

prepend greeting 0 0 6
hello 
# -> STORED

get greeting
# VALUE greeting 0 11
# hello world
# END

# prepend to non-existent key
prepend nope 0 0 3
abc
# -> NOT_STORED

# flags/exptime ignored — only the value bytes change

Storage Commands Comparison

Memcached's six storage commands (set, add, replace, append, prepend, cas) differ only in their precondition for storing. set is unconditional; add/replace are existence-gated; append/prepend modify in place and ignore metadata; cas is gated on the CAS token from gets. Picking the right one turns a read-modify-write race into a single atomic server operation.

memcached
# When does each command succeed?
#
#  set     -> ALWAYS (create or overwrite)
#  add     -> only if key does NOT exist
#  replace -> only if key ALREADY exists
#  append  -> only if key exists (adds to end, ignores flags/exptime)
#  prepend -> only if key exists (adds to start, ignores flags/exptime)
#
# All return STORED / NOT_STORED / EXISTS / NOT_FOUND as appropriate.

# set:        overwrite unconditionally
# add:        lazy init (counters, locks)
# replace:    refresh only if already cached
# append:     grow a buffer / log
# prepend:    stack headers

# all accept [noreply] to skip the response
04

CAS (Compare-And-Swap)

gets — Fetch the CAS Token

gets (get-extended) is the read half of CAS. It returns the value plus a cas_unique 64-bit token that the server guarantees changes on every modification of that key. You capture this token and hand it back to cas; if the token still matches, your write succeeds. Without gets you cannot do a safe CAS — a plain get omits the token.

memcached
# gets is get + a 64-bit cas_unique token
gets counter
# VALUE counter 0 1 7
# 5
# END
#                ^ cas_unique = 7

# the token is opaque and changes on EVERY write to the key
set counter 0 0 1
5
gets counter
# VALUE counter 0 1 8      <- token changed to 8

# fetch multiple keys with tokens in one call
gets a b c
# VALUE a 0 3 11
# ...
# VALUE c 0 3 19
# ...

cas — Conditional Write

cas writes a value only if the cas_unique token still matches what gets returned. STORED means the write applied; EXISTS means another client modified the key first (token mismatch); NOT_FOUND means the key was deleted. This is optimistic locking — retry the whole gets→modify→cas loop on EXISTS. It avoids lost updates without server-side locking.

memcached
# syntax: cas <key> <flags> <exptime> <bytes> <cas_unique> [noreply]
gets counter
# VALUE counter 0 1 8       <- token is 8
# 5

cas counter 0 0 1 8
6
# -> STORED   (token matched, write applied)

# a concurrent write changed the token in the meantime
cas counter 0 0 1 8        # stale token 8, but key is now on token 9
6
# -> EXISTS  (token mismatch, someone else wrote first)

CAS Use Case: Safe Counter

CAS solves the lost-update problem when you need read-modify-write semantics (e.g. updating a JSON blob inside a cache entry). On EXISTS you must re-read with gets, re-apply your transformation, and retry cas — typically with a bounded retry count to avoid live-lock. For pure integer counters, incr/decr are simpler and atomic; reach for CAS only when the value is structured.

memcached
# read-modify-write with get+set is RACY:
#   get counter -> 5      (client A)
#   get counter -> 5      (client B)
#   set counter 6         (A writes 6)
#   set counter 6         (B writes 6, LOST A's increment!)

# safe pattern with gets/cas:
gets counter               # token = 8, value = 5
newval = 5 + 1
cas counter 0 0 1 8        # 8 -> STORED, value = 6

# on EXISTS, retry the whole loop:
gets counter               # token = 9, value = 6
cas counter 0 0 1 9        # -> STORED, value = 7

# for plain counters prefer incr/decr (atomic, no CAS needed)

CAS Failure & Retry

A robust CAS loop must handle three outcomes: missing key (use add, fall back to retry on NOT_STORED), token match (STORED, done), and token mismatch (EXISTS, retry). Cap retries to avoid live-lock under contention. If the key is genuinely missing, prefer add over set so two clients racing to create it don't clobber each other.

memcached
# pseudocode for a CAS retry loop
max_retries = 5
for i in range(max_retries):
    res = client.gets(key)          # value + cas token
    if res is None:
        # key missing -> use add to create it race-free
        if client.add(key, new_value, ttl):
            break
        else:
            continue                # someone else created it; retry
    old_val, cas_token = res
    new_val = transform(old_val)    # your read-modify logic
    if client.cas(key, new_value, ttl, cas_token):
        break                       # STORED
    # else EXISTS -> loop and retry
else:
    raise Exception("CAS failed after retries")

CAS Limitations

CAS is single-key only — Memcached has no multi-key transactions like Redis MULTI/EXEC. It is optimistic, not pessimistic: under heavy contention many writes will hit EXISTS and retry, so shard hot keys. The cas_unique is per-key and opaque; never compare it across keys or assume ordering. If a key is evicted and re-created the token resets, so treat NOT_FOUND as a fresh start rather than a failure.

memcached
# 1) CAS only protects a SINGLE key — no multi-key transactions
#    (memcached has no MULTI/EXEC like Redis)
#
# 2) CAS tokens are per-key and opaque — never compare across keys
#
# 3) CAS does NOT lock the key; it's optimistic — high contention
#    means many retries (consider sharding the hot key)
#
# 4) The binary protocol exposes cas more cleanly; some ASCII quirks
#    exist in older servers
#
# 5) cas_unique may RESET if a key is evicted and re-created —
#    treat absence (NOT_FOUND) as a fresh start, not a token failure

# for multi-key atomicity, use app-level locks (e.g. add-based lock)
05

Counters (incr / decr)

incr — Atomic Increment

incr atomically adds a positive integer to a key whose value is a decimal string, returning the new value. The operation is fully atomic across concurrent clients — no lost updates. The value must be 64-bit unsigned numeric ASCII (no signs, no decimals). incr on a missing key returns NOT_FOUND; on non-numeric data it errors. There is no auto-create — use add first.

memcached
# syntax: incr <key> <value> [noreply]
# the key's value MUST be a numeric string (ASCII digits)
set views 0 0 1
5
# -> STORED

incr views 1
# -> 6

incr views 10
# -> 16

# returns the NEW value as a decimal string

# incrementing a non-numeric value
set name 0 0 5
alice
incr name 1
# -> CLIENT_ERROR cannot increment or decrement non-numeric value

# incr on a missing key
incr missing 1
# -> NOT_FOUND

decr — Atomic Decrement

decr atomically subtracts from a numeric value and returns the result. It is floor-clamped at 0 — it will never produce a negative number, which is convenient for stock/inventory counters but means you cannot use decr to track net-negative balances. Like incr it requires the value to be ASCII digits and the key to exist (else NOT_FOUND).

memcached
# syntax: decr <key> <value> [noreply]
set views 0 0 2
16
# -> STORED

decr views 1
# -> 15

decr views 10
# -> 5

# decr NEVER goes negative — stops at 0
decr views 100
# -> 0

# decr a missing key
decr missing 1
# -> NOT_FOUND

# decr a non-numeric value -> CLIENT_ERROR

Initializing Counters

Because incr/decr return NOT_FOUND on a missing key, counters must be initialized with add (race-free: the first caller creates at 0, others get NOT_STORED and skip straight to incr). Never use set to initialize a counter you also incr — set overwrites unconditionally and races with concurrent increments. Namespacing counters by date and setting a TTL lets them auto-expire.

memcached
# incr/decr do NOT create keys — initialize with add first

# race-free counter init: add creates at "0" only if absent
add counter:daily 0 86400 1
0
# -> STORED (first caller wins) or NOT_STORED (already exists)

# now safe to increment from any client
incr counter:daily 1
# -> 1

# alternative: set then incr (but set+incr is racy for resets)
# For a daily counter, key by date so it auto-expires:
#   counter:2024-01-01
#   counter:2024-01-02

Counter Use Cases

Counters shine for rate limiting (key by IP+minute with a 60s TTL), view/download stats, daily-active counts, inventory, and rollout metrics. They are atomic and cheap, so they scale to very high write rates. For rate limiting, choose a key granularity (per-user, per-IP) and TTL that matches your window — and remember decr floors at 0, so inventory can't go negative.

memcached
# rate limiting (per IP per minute)
add ratelimit:1.2.3.4:2024010112 0 60 1
0
incr ratelimit:1.2.3.4:2024010112 1
# if result > 100 -> reject request

# view/download counters
incr article:42:views 1

# daily active users (with a SET of user ids tracked elsewhere)
incr dau:20240101 1

# inventory / stock (decrement on purchase)
decr product:42:stock 1
# if result == 0 -> out of stock

# feature-flag rollout counters
incr flag:newui:shown 1

Counter Limitations

Counters are unsigned 64-bit integers only — no floats, no negatives, no signs. incr/decr never auto-create a key. decr is clamped at 0. Because the value is just a string, a stray set/replace will overwrite it and clobber concurrent increments — initialize with add and otherwise only ever use incr/decr. For floats or multi-field counters, use CAS on a structured value instead.

memcached
# 1) values must be unsigned 64-bit integers (no decimals, no signs)
#    incr/decr on "3.14" or "-5" -> CLIENT_ERROR
#
# 2) no auto-create — must add/set first; incr on missing = NOT_FOUND
#
# 3) decr floors at 0 — cannot represent negative balances
#
# 4) the counter is just a string; set/add/replace/cas OVERWRITE it,
#    which can clobber concurrent incr/decr (use add only to init)
#
# 5) for float counters or complex updates, use CAS on a JSON blob

# to RESET a counter, use set (not decr-to-zero) — but watch for races
06

Expiration & TTL

set with Expiration

exptime is in seconds: 0 means never expire, values up to 2592000 (30 days) are relative TTLs, and larger values are interpreted as absolute Unix timestamps. Expired items are not actively reaped at the exact second — Memcached uses lazy expiration (checked on access) plus a background LRU crawler, so a stale key may linger until touched or evicted.

memcached
# exptime is the 4th arg to all storage commands (seconds)
# 0 = no expiration (live until evicted)

set session 0 0 5           # never expire
alice

set session 0 1800 5        # expire in 1800 seconds (30 min)
alice

# exptime > 30 days (2592000 s) is treated as a UNIX timestamp
set archive 0 1735689600 5  # expires at 2025-01-01 00:00 UTC
alice

# common TTLs
#   60      -> 1 minute  (rate limit windows)
#   3600    -> 1 hour    (short cache)
#   86400   -> 1 day     (daily cache)
#   0       -> forever   (until LRU evicts)

touch — Update TTL

touch (1.4.8+) updates a key's TTL in place without transferring the value — far cheaper than get+set for sliding-window sessions. Set exptime to 1 to soft-delete (it becomes immediately expired). The same 30-day relative-vs-absolute rule applies. touch returns NOT_FOUND for missing keys, so callers should treat that as 'not cached'.

memcached
# touch: change a key's TTL without fetching the value
# syntax: touch <key> <exptime> [noreply]

set session 0 3600 5
alice

# extend the session by another hour
touch session 3600
# -> TOUCHED

# make it never expire
touch session 0
# -> TOUCHED

# expire it immediately (poor man's delete via TTL)
touch session 1
# -> TOUCHED   (effectively deleted on next access)

touch missing 60
# -> NOT_FOUND

gat & gats — Get and Touch

gat (1.6+) combines get and touch atomically in a single round-trip — perfect for session reads that should slide the expiration forward. gats also returns the CAS token. Using get+touch separately creates a window where the TTL isn't refreshed and doubles the round-trips; gat fixes both. For multi-key sliding reads, gat accepts several keys.

memcached
# gat: fetch value AND update TTL in one round-trip
# syntax: gat <exptime> <key>

gat 3600 session
# VALUE session 0 5
# alice
# END

# gats: get-and-touch with CAS token
gats 3600 session
# VALUE session 0 5 42
# alice
# END

# ideal for session reads that should refresh the TTL:
#   - one network round-trip instead of get + touch
#   - atomic: no window where TTL isn't refreshed

# multiple keys: gat <exptime> <key1> <key2> ...
gat 3600 a b c

Expiration Behavior

Expiration in Memcached is lazy by default — a key is only reaped when accessed, which means stale data can occupy memory until evicted. The LRU crawler (1.4.24+) adds a background sweep that proactively frees expired items per slab, preventing memory waste. Enable it in production: lru_crawler enable. Even so, never rely on precise expiry timing for correctness.

memcached
# Memcached does NOT actively scan for expired keys every second.
# Expiration is LAZY: a key is checked when accessed (get/gets/touch).
#
# A background "LRU crawler" (1.4.24+) periodically sweeps slabs to
# free expired items so memory is reclaimed even if never accessed.
#
#   stats lru_crawler           # is the crawler enabled?
#   lru_crawler enable          # turn it on
#   lru_crawler sleep 100       # microseconds between slab visits
#   lru_crawler tocrawl 1000    # max items to inspect per slab per pass

# an expired-but-not-yet-reaped key:
get expiredkey
# -> END   (treated as missing, memory freed for its slab)

TTL Best Practices

Always set a TTL — unbounded cache entries cause unbounded memory growth and stale-data bugs. Add random jitter (±5–10%) to TTLs so correlated expirations don't stampede the origin. Namespace volatile data by date so old entries auto-expire. On a miss, recompute and store in the same code path to keep the cache warm. Use delete, not TTL=1, for explicit invalidation.

memcached
# 1) ALWAYS set a TTL on cache entries — never store forever (0)
#    unless you have an explicit invalidation path.
#
# 2) add jitter to avoid thundering herds:
ttl = base_ttl + random(0, 300)   # +/- 5 minutes
#
# 3) namespace by time so old keys auto-expire:
set report:2024-01-01 0 86400 N   # yesterday's report, 1-day TTL
#
# 4) use longer TTLs for stable data, shorter for volatile data
#
# 5) on a cache miss, recompute AND set with TTL in ONE path
#
# 6) don't use exptime=1 as a "delete" in production — use delete
07

Stats Commands

stats — Overview

stats dumps the core server metrics: cmd_get/cmd_set (total ops), get_hits/get_misses (hit rate = hits/cmd_get), evictions (items dropped due to memory pressure), curr_items, bytes vs limit_maxbytes, and connection counts. Hit rate and evictions are the two numbers to watch first. This is the foundation of any Memcached monitoring.

memcached
# general server statistics
stats
# STAT pid 1234
# STAT uptime 3600
# STAT curr_connections 10
# STAT total_connections 1024
# STAT cmd_get 50000
# STAT cmd_set 12000
# STAT get_hits 45000
# STAT get_misses 5000
# STAT evictions 12
# STAT bytes 8388608
# STAT limit_maxbytes 67108864
# STAT curr_items 4321
# ...
# END

# hit rate = get_hits / cmd_get  (here 90%)
# watch evictions — rising = memory pressure

stats items — Per-Slab Item Counts

stats items reports, for each slab class, how many items it holds (number) and the age of its least-recently-stored item (age). A slab with a high number but low age is being churned; a slab with high age holds long-lived data. This is the first place to look when tuning chunk sizes or diagnosing why a particular size range is being evicted.

memcached
# item counts and age per slab class
stats items
# STAT items:1:number 100
# STAT items:1:age 1200
# STAT items:2:number 250
# STAT items:2:age 3600
# STAT items:5:number 3
# STAT items:5:age 60
# ...
# END

# items:<slabid>:number  -> how many items live in that slab class
# items:<slabid>:age     -> age of the LRU item (seconds since stored)
# use this to see which size classes are full / hot

stats slabs — Slab Allocator State

stats slabs reveals the slab allocator's layout: each class has a chunk_size (item slots are rounded up to it), chunks_per_page, total_pages, and used/free chunks. Memory assigned to a slab class stays there — it cannot be returned to other classes. If one class is full and evicting while another is empty, you have slab calcification, addressable with -f (growth factor) tuning.

memcached
# memory layout of each slab class
stats slabs
# STAT 1:chunk_size 96
# STAT 1:chunks_per_page 10922
# STAT 1:total_pages 1
# STAT 1:total_chunks 10922
# STAT 1:used_chunks 100
# STAT 1:free_chunks 10822
# STAT 2:chunk_size 120
# ...
# STAT active_slabs 5
# STAT total_malloced 16777216
# END

# chunk_size    -> bytes per item slot in this class
# total_chunks  -> total slots
# used_chunks   -> slots holding data
# free_chunks   -> empty slots

stats sizes — Item Size Histogram

stats sizes builds a histogram of real item sizes (key+value+overhead) so you can see how much memory is wasted to chunk-size rounding. It performs a full scan and historically locked the server — modern versions gate it behind a safe-mode check; pass --disable-safe-mode to force it. Run only during low traffic. The output helps pick the right -f growth factor.

memcached
# histogram of actual item sizes (key + value + overhead)
# WARNING: this command locks the cache while scanning — do NOT run
# in production at peak. Use stats sizes --disable-safe-mode only
# if you accept the risk.

stats sizes
# STAT 96 100       # 100 items rounded up to 96 bytes
# STAT 120 250
# STAT 192 50
# ...
# END

# compare to stats slabs to see how much memory is wasted to
# chunk-size rounding (internal fragmentation)

stats cachedump — Inspect a Slab

stats cachedump lists the keys held in a given slab class (limited to N entries), useful for ad-hoc debugging. It walks the slab's LRU and can be slow or disabled entirely in some builds — never use it for production key enumeration. Memcached deliberately offers no efficient way to list all keys; treat it as a black box keyed by your application.

memcached
# peek at keys in a specific slab class
# syntax: stats cachedump <slab_id> <limit> [noreply]
stats cachedump 1 10
# ITEM user:1 [5 b; 1700000000 s]
# ITEM user:2 [5 b; 1700000000 s]
# ...
# END

# WARNING: for debugging only — it walks the LRU and can be slow.
# Not all builds ship cachedump; it is disabled in some distros.
# Each ITEM shows: key, [value size in bytes; storage time s]

# do NOT use this for key enumeration in production code —
# memcached has no efficient SCAN command

stats reset & Other Subcommands

stats reset zeroes the cumulative counters (ops, hits, misses) without dropping data — handy for measuring a fresh interval after a deploy. Other useful subcommands: stats settings (effective config), stats conns (per-connection detail on newer builds), stats lru_crawler (crawler state). Counters are otherwise cumulative since process start, so reset before benchmarking.

memcached
# zero the cumulative counters (cmd_get, hits, misses, etc.)
stats reset
# -> RESET

# useful stats subcommands:
stats settings      # current effective configuration
stats items         # per-slab item counts/age
stats slabs         # slab allocator layout
stats sizes         # item size histogram (slow!)
stats conns         # per-connection info (newer builds)
stats lru_crawler   # crawler state

# reset counters after a deploy to measure only current run
# (counters are since process start otherwise)
08

Settings & Configuration

stats settings — Effective Config

stats settings shows the running configuration: maxbytes (-m), maxconns (-c), growth_factor (-f), item_size_max (-I), whether evictions and CAS are enabled, and the LRU crawler state. Most of these are fixed at startup; only a handful can be changed at runtime. This is the first place to confirm whether your server picked up the flags you think it did.

memcached
# view the server's current effective settings
stats settings
# STAT maxbytes 67108864
# STAT maxconns 1024
# STAT tcpport 11211
# STAT udpport 0
# STAT inter NULL
# STAT evictions on
# STAT cas_enabled on
# STAT chunk_size 48
# STAT growth_factor 1.25
# STAT item_size_max 1048576
# STAT lru_crawler on
# ...
# END

# this reflects flags passed at startup (-m, -c, -f, -I, ...)

Startup Flags

Most configuration happens via startup flags. The critical ones: -m (memory), -l (always bind to 127.0.0.1 or a private network — never expose Memcached publicly), -c (connection cap), -I (max item size), and -f (slab growth factor). -M flips eviction behavior to error-on-full instead of LRU eviction (rarely wanted for a cache). Tune -t to your CPU count for throughput.

memcached
# common memcached startup flags
memcached -d -m 256 -p 11211 -u memcache -c 2048 -f 1.25 -I 1m

#  -d              run as a daemon
#  -m <MB>         max memory to use for items (default 64)
#  -M              return error instead of evicting when memory full
#  -c <conn>       max simultaneous connections (default 1024)
#  -p <port>       TCP port (default 11211)
#  -U <port>       UDP port (0 disables; default 11211)
#  -l <addr>       listen address (default all — bind to 127.0.0.1!)
#  -u <user>       run as this user (when started as root)
#  -f <factor>     slab growth factor (default 1.25)
#  -n <bytes>      min item allocation (default 48)
#  -I <size>       max item size (default 1m, can be 1m-1g)
#  -t <threads>    worker threads (default 4)
#  -v / -vv / -vvv verbosity

Runtime Configuration

Memcached exposes only a few runtime knobs: cache_memlimit (resize the memory cap live), the lru_crawler family (enable/disable/tune the background reaper), verbosity (log level), and flush_all (invalidate all items, optionally delayed). Most settings remain fixed at startup — to change -m, -c, or -I you restart the process. flush_all is a control-plane op, not a setting.

memcached
# a small number of settings can change at runtime:

# change the memory limit (bytes) live
cache_memlimit 536870912      # 512 MB
# -> OK

# LRU crawler controls
lru_crawler enable            # turn the crawler on
lru_crawler disable           # turn it off
lru_crawler sleep 100         # us between slab visits
lru_crawler tocrawl 1000      # max items per slab per pass
lru_crawler metadump all      # dump key metadata (not values)

# log verbosity at runtime
verbosity 2

# flush_all (invalidate everything, not a config change)
flush_all
flush_all 900                 # invalidate in 900 seconds

Important Settings Explained

-m is the headline setting: size it to your working set plus headroom, since undersizing causes evictions and cache churn. -f trades slab-class granularity against internal fragmentation — lower it (toward 1.1) when you have many narrowly-sized items. -I caps item size; raise it only if you must cache large blobs. -M is rarely used for a cache — it turns OOM into errors instead of evictions.

memcached
# -m  memory budget
#    the single most important knob. Size to fit your working set
#    with headroom; undersizing = evictions = cache churn.
#
# -f  slab growth factor (default 1.25)
#    smaller (1.1) = more slab classes, less internal fragmentation
#    but more per-class memory stuck. Larger (1.5) = fewer classes,
#    more waste per chunk.
#
# -I  max item size (default 1m)
#    raise to cache bigger blobs (e.g. -I 4m) at the cost of larger
#    worst-case allocations.
#
# -M  disable LRU eviction (error instead) — only for strict caches
#
# -t  worker threads (default 4) — scale to CPU count for throughput

Connection Limits & Threads

-c caps concurrent connections — set it comfortably above (pool_size × app_instances) or clients will be refused (watch listen_disabled_num in stats). -t sets worker threads; Memcached is event-driven per thread, so scale -t to CPU cores, typically 4–8. Beyond that, lock contention limits gains. Each thread owns a slice of the hash table, so more threads don't help a single hot key.

memcached
# -c  max client connections (default 1024)
memcached -c 4096

# each connection costs a bit of memory; set -c above your expected
# peak concurrent clients (pool size * app instances + headroom)

# -t  worker threads (default 4)
memcached -t 8
# memcached is event-driven per thread; each thread runs its own
# event loop and owns a slice of the hash table. Scale -t to CPU
# cores. Beyond ~8 threads, contention often limits gains.

# check connection pressure:
stats
# STAT curr_connections 412
# STAT total_connections 10230
# STAT listen_disabled_num 0   # >0 means you hit -c and refused conns
09

Caching Strategies

Cache-Aside (Lazy Loading)

Cache-aside is the simplest and most popular pattern: the app reads the cache first, and on a miss loads from the DB and back-fills the cache. It's resilient (a cache outage just means more DB load) but allows stampedes on cold keys and staleness until explicit invalidation. Always set a TTL even when invalidating on write, as a safety net for missed invalidations.

memcached
# the most common pattern — app manages the cache explicitly

def get_user(id):
    key = "user:" + str(id)
    val = mc.get(key)              # 1) try cache
    if val is not None:
        return val                 #    hit
    val = db.query(id)             # 2) miss -> load from DB
    mc.set(key, val, ttl=3600)     # 3) back-fill the cache
    return val

# on write:
def update_user(id, data):
    db.update(id, data)
    mc.delete("user:" + str(id))   # invalidate (or set fresh value)

# pros: simple, resilient to cache failure
# cons: stampede on miss; stale until invalidated

Read-Through

In read-through, the cache layer itself invokes a loader on a miss — the app just calls get and gets a value. This centralizes cache-population logic and guarantees consistency across callers, at the cost of a synchronous DB read inside the cache call. Most Memcached clients don't ship read-through built-in, so it's usually implemented as a thin wrapper around cache-aside.

memcached
# the cache itself is responsible for loading from the DB
# (the client library or a cache layer provides a loader callback)

mc = memcached_with_loader(load_fn=db.query_user)

def get_user(id):
    # the cache calls load_fn(id) automatically on a miss
    return mc.get_or_load("user:" + str(id))

# pros:
#   - app code is simpler (no miss-handling logic)
#   - cache population is consistent across callers
# cons:
#   - the cache library must support a loader (or you wrap it)
#   - a cache miss is slower (synchronous DB read inside get)

Write-Through

Write-through updates the cache on every write, keeping it fresh with no separate invalidation path. The trade-off is write latency (cache + DB) and a failure-ordering risk: if you write cache-first and the DB write fails, the cache holds data that isn't durable; write DB-first and there's a staleness window. Most apps combine write-through for hot keys with cache-aside for the rest.

memcached
# every write goes to the cache AND the DB synchronously

def update_user(id, data):
    mc.set("user:" + str(id), data, ttl=3600)   # cache first
    db.update(id, data)                          # then DB
    # or DB first, then cache — pick an order and stick with it

# pros:
#   - cache stays fresh after writes (no stale reads)
#   - no invalidation logic needed
# cons:
#   - write latency = cache write + DB write
#   - if the DB write fails after the cache write, cache is inconsistent
#   - cache holds data even if never read (wastes memory)

Write-Behind (Write-Back)

Write-behind writes to the cache immediately and persists to the DB asynchronously via a queue — giving sub-millisecond write latency and spike absorption. The cost is consistency (the DB lags the cache) and durability (a crash before the worker drains loses data). Memcached alone can't do this; you need a queue + worker. Reserve it for write-heavy, loss-tolerant workloads like counters and telemetry.

memcached
# writes go to the cache first; DB is updated ASYNCHRONOUSLY later
# (memcached alone can't do this — it's a cache, not a queue.
#  this pattern needs a worker/queue alongside it.)

def update_user(id, data):
    mc.set("user:" + str(id), data, ttl=3600)
    queue.push({"op":"update","id":id,"data":data})   # async DB write

# a background worker drains the queue to the DB

# pros:
#   - very fast writes (cache-only latency)
#   - absorbs write spikes
# cons:
#   - DB lags behind cache -> reads may see newer data than DB
#   - data loss if cache evicts/evacuates before the worker runs
#   - complex (queue + worker + reconciliation)

Cache Invalidation

Invalidation is the hardest part of caching. Explicit delete-on-write is the baseline; TTL is the safety net (always set one). For collections you can't enumerate, use versioned keys (user:1:v42) and bump the version on schema/data changes so old entries age out. Time-bucketed keys (report:2024-01-01) auto-expire by date. Never try wildcard invalidation — Memcached can't list keys.

memcached
# invalidation is HARD — "there are only two hard problems in CS"

# 1) explicit invalidation on write
def update_user(id, data):
    db.update(id, data)
    mc.delete("user:" + str(id))     # drop stale entry

# 2) versioned keys to avoid invalidation entirely
key = "user:" + str(id) + ":v" + str(version)
# bump version on update -> old keys auto-expire via TTL

# 3) TTL as a safety net — even with explicit invalidation, set a TTL
#    so a missed invalidation self-heals within (e.g.) an hour

# 4) namespacing by time/date for time-bucketed data
key = "report:" + today_date()      # yesterday auto-expires

# anti-pattern: trying to invalidate "all user:* keys" —
# memcached can't enumerate keys, so use a versioned namespace instead

TTL & Eviction Strategies

Match TTL to data volatility: hours for reference data, minutes for user data, the window length for rate-limit counters. Always add jitter (±5–10%) to avoid synchronized expiry stampedes. Memcached evicts LRU per slab under memory pressure with no per-item pinning — the only levers are TTL and total sizing. For hot keys, use an add-based lock so only one client recomputes on a miss.

memcached
# choose TTL by data volatility:
#   reference data (rarely changes)   -> 1-24 hours
#   user data (changes on action)     -> 5-60 minutes
#   computed reports                  -> matches refresh cadence
#   sessions                          -> matches idle timeout
#   rate-limit counters               -> matches the window (60s)

# add jitter to prevent synchronized expiry / stampedes:
ttl = base + random.randint(-60, 60)

# on high memory pressure, Memcached evicts LRU items per slab.
# to AVOID eviction of critical data, give it a longer TTL OR
# store it under a key pattern you can pin (memcached has no
# explicit "no-evict" flag — TTL + sizing are your tools).

# stampede protection: lock-and-recompute on miss
if mc.add("lock:user:1", "1", ttl=30):
    val = db.query(1); mc.set("user:1", val, 3600); mc.delete("lock:user:1")
else:
    val = mc.get("user:1")  # another client is refreshing; retry shortly
10

Client Libraries

Python (pymemcache)

pymemcache is pure-Python, fast, and explicit. Use the plain Client for a single server and HashClient for a cluster with consistent hashing. set/get are the basics; get_many batches a multi-get. Use add+incr for counters and gets/cas for optimistic updates. Always pass explicit timeouts — a dead node should fail fast, not hang your request thread.

memcached
# pip install pymemcache
from pymemcache.client.base import Client

mc = Client(("127.0.0.1", 11211), connect_timeout=2, timeout=1)

mc.set("user:1", "alice", expire=3600)
print(mc.get("user:1"))   # b"alice"

# multi-get (batched)
for k, v in mc.get_many(["user:1", "user:2", "user:3"]).items():
    print(k, v)

# counter
mc.add("views", 0)
mc.incr("views", 1)

# CAS loop
while True:
    val, cas = mc.gets("counter")
    if mc.cas("counter", int(val) + 1, cas, expire=0):
        break

# use a consistent-hashing cluster
from pymemcache.client.hash import HashClient
cluster = HashClient([("mc1", 11211), ("mc2", 11211), ("mc3", 11211)])

Node.js (memcached)

The memcached npm package supports single and multi-server (consistent hashing) setups, connection pooling, and retries. set/get/getMulti (batched multi-get) cover the common path. Configure poolSize to your concurrency and always set a timeout so a dead node fails fast. The library is callback-style; wrap it in Promises or use util.promisify for async/await ergonomics.

memcached
// npm install memcached
const Memcached = require("memcached");

const mc = new Memcached("127.0.0.1:11211", {
  timeout: 1000,
  retries: 1,
  poolSize: 10,
});

mc.set("user:1", "alice", 3600, (err) => {
  if (err) console.error(err);
});

mc.get("user:1", (err, data) => {
  console.log(data); // "alice"
});

// multi-get
mc.getMulti(["user:1", "user:2"], (err, data) => {
  console.log(data["user:1"]);
});

// counter
mc.incr("views", 1, (err, val) => console.log(val));

// cluster: pass a comma-separated list, client does consistent hashing
const cluster = new Memcached("mc1:11211,mc2:11211,mc3:11211");

mc.end(); // close connections on shutdown

PHP (memcached extension)

PHP's memcached extension (built on libmemcached) is the production choice — set OPT_LIBKETAMA_COMPATIBLE for consistent hashing across servers, and addServers for a cluster. getMulti batches reads. cas() takes the token from fetch()'s result array. Avoid the older memcache extension (no 'd'); it lacks features and is less maintained. Always set TTLs and handle Memcached::RES_NOTFOUND cleanly.

memcached
<?php
// requires the memcached extension (libmemcached-based)
$mc = new Memcached();
$mc->addServer("127.0.0.1", 11211);

$mc->set("user:1", "alice", 3600);
echo $mc->get("user:1");   // alice

// multi-get returns associative array
$rows = $mc->getMulti(["user:1", "user:2", "user:3"]);

// counter
$mc->set("views", 0);
$mc->increment("views", 1);

// CAS
$mc->getDelayed(["counter"], false, null);
while ($item = $mc->fetch()) {
    $cas = $item["cas"];
    $mc->cas($cas, "counter", intval($item["value"]) + 1);
}

// cluster with consistent hashing
$mc->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
$mc->addServers([
    ["mc1", 11211],
    ["mc2", 11211],
    ["mc3", 11211],
]);

Ruby (dalli)

Dalli is the canonical Ruby Memcached client — pure-Ruby, thread-safe, and the default Rails cache store. Pass an array of servers for consistent-hashing (ketoma) clustering. Its cas block form re-reads and retries on conflict automatically, which is far more ergonomic than manual loops. Configure expires_in globally and override per-call for tighter TTLs.

memcached
# gem install dalli
require "dalli"

# single server
mc = Dalli::Client.new("127.0.0.1:11211", { expires_in: 3600 })

mc.set("user:1", "alice")
mc.get("user:1")          # => "alice"

# multi-get (returns a hash)
mc.get_multi(["user:1", "user:2", "user:3"])

# counter
mc.incr("views", 1)
mc.decr("views", 1)

# CAS
mc.get("counter") do |val, cas|
  mc.cas("counter", cas) { val.to_i + 1 }
end

# cluster with consistent hashing (ketama)
cluster = Dalli::Client.new(["mc1:11211", "mc2:11211", "mc3:11211"],
                            { expires_in: 3600 })

Java (spymemcached)

spymemcached is a high-performance, async, NIO-based Java client. Its API is Future-based; get() blocks when you need confirmation. Pass a space-separated server list to AddrUtil for consistent hashing. gets()/cas() provide optimistic concurrency, and getBulk() does a batched multi-get. Always shutdown() on app teardown to release the IO threads.

memcached
// build.gradle: implementation 'net.spy:spymemcached:2.12.3'
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.AddrUtil;

MemcachedClient mc = new MemcachedClient(
    AddrUtil.getAddresses("mc1:11211 mc2:11211 mc3:11211"));

// async API returns java.util.concurrent.Future
Future<Boolean> f = mc.set("user:1", 3600, "alice");
f.get(); // block for confirmation

Object val = mc.get("user:1");    // "alice"

// counter
mc.incr("views", 1);

// CAS
CASValue<Object> cv = mc.gets("counter");
CASResponse r = mc.cas("counter", cv.getCas(), newVal);
if (r == CASResponse.OK) { /* success */ }

// bulk get
Map<String, Object> rows = mc.getBulk("user:1", "user:2", "user:3");

mc.shutdown();

Go (gomemcache)

gomemcache (bradfitz) is the de facto Go client — simple, fast, and supports consistent hashing across multiple servers. Set/Get take an Item struct that bundles key, value, TTL, and the CAS token; CompareAndSwap does the optimistic update. GetMulti returns a map keyed by name (missing keys are absent, not errors). It's concurrency-safe; reuse one client across goroutines.

memcached
// go get github.com/bradfitz/gomemcache/memcache
import memcache "github.com/bradfitz/gomemcache/memcache"

mc := memcache.New("mc1:11211", "mc2:11211", "mc3:11211")

// set
err := mc.Set(&memcache.Item{
    Key: "user:1", Value: []byte("alice"), Expiration: 3600,
})

// get
it, err := mc.Get("user:1")
// it.Value -> []byte("alice")

// multi-get (returns map, missing keys absent)
rows, _ := mc.GetMulti([]string{"user:1", "user:2"})

// counter
mc.Increment("views", 1)
mc.Decrement("views", 1)

// CAS
it, _ = mc.Get("counter")
it.Value = []byte("6")
it.Cas = it.Cas   // use existing token
mc.CompareAndSwap(it)

// delete
mc.Delete("user:1")
11

Distributed Architecture

Consistent Hashing

Consistent hashing maps both servers and keys onto a ring; a key is owned by the next server clockwise. Adding or removing a server moves only that server's slice of keys (about 1/N), not the whole keyspace — far better than hash(key) % N which remaps nearly everything on membership change. Most clients use the ketama variant with many virtual nodes per server for balanced distribution.

memcached
# consistent hashing spreads keys across servers with minimal
# remapping when the pool changes.
#
# 1) hash each server onto a ring (multiple virtual nodes each)
# 2) hash the key, walk the ring clockwise to the next server
#
#   ring:  s1---s2---s3---s1
#          ^         ^
#        keyA       keyB  -> goes to s3
#
# adding s4 only moves keys between s3 and s4 (not the whole ring)
# removing s1 only moves keys between s1's predecessor and s2
#
# result: |keys moved| ~= keys/N when adding/removing one server
# (vs. ~all keys remapped with hash(key) % N)

# most clients implement ketama consistent hashing automatically
# when you pass a list of servers

Client-Side Sharding

All sharding lives in the client: it hashes each key to one server and fans out multi-gets across servers in parallel. Each key exists on exactly one node — there is no replication, so a dead node loses its slice of the keyspace. There's no proxy or coordinator; every client must share the same server list and hashing config (ketama) or keys will land on different nodes and cause mass misses.

memcached
# memcached servers do NOT coordinate — sharding is 100% client-side.
#
# the client picks ONE server per key:
#   server = ring[hash(key)]
#
# a multi-get is split across servers in parallel:
#   getMulti(["a","b","c"])
#     -> get "a" from mc1
#     -> get "b" from mc2   (concurrent, fan-out)
#     -> get "c" from mc1
#
# this means:
#   - each key lives on exactly ONE server (no replication)
#   - a multi-get touches multiple servers but returns one result set
#   - the client must know the full server list (no proxy)

# never put a replica behind the same client as a primary for the
# same key range — memcached clients do not read-replicate

Hashing Algorithms (Ketama)

Ketama is the canonical memcached consistent-hashing scheme: each physical server is mapped onto the ring as many virtual nodes (~160) for balanced load. Locating a key hashes it and walks clockwise to the next node. The critical gotcha: every client in every language must use the same algorithm and identical server-label strings, or they'll disagree on ownership and cause mass cache misses after a deploy.

memcached
# ketama is the de-facto consistent-hashing algorithm for memcached.
#
# for each server, create N virtual nodes (default ~160-200):
#   for i in range(vnodes):
#       point = hash("server:port-" + i)
#       ring[point] = server
#
# to locate a key:
#   point = hash(key)
#   server = first node clockwise from point on the ring
#
# benefits of many virtual nodes:
#   - balanced load (no single server gets a huge arc)
#   - smooth redistribution on add/remove
#
# IMPORTANT: all clients must use the SAME algorithm + server labels,
# or they will disagree on which server owns a key.

# enable in clients:
#   PHP:   Memcached::OPT_LIBKETAMA_COMPATIBLE
#   Ruby:  default in dalli
#   Python: HashClient (pymemcache)

Adding & Removing Servers

With consistent hashing, adding or removing a server remaps only ~1/N of keys — far better than modulo hashing which remaps nearly everything. But a server crash still permanently loses its keys (no replication), causing a transient spike in origin load as those entries repopulate. Roll out membership changes to all clients at once, and consider pre-warming a new node by shadowing reads before it goes live.

memcached
# adding a server to the ring:
#   - only ~1/N of keys remap (the slice the new server now owns)
#   - those keys are cold misses until repopulated -> brief spike
#
# removing a server (e.g. crash):
#   - that server's keys are GONE (no replication)
#   - its slice of the ring is reassigned to neighbors
#   - all reads to those keys miss until repopulated
#
# best practices for membership changes:
#   1) deploy new server list to all clients simultaneously
#   2) warm a new server with a shadow-read phase if possible
#   3) expect a transient increase in DB load (cache misses)
#   4) use weighted virtual nodes to add capacity gradually

# anti-pattern: hash(key) % N  -> changes ALL key placements on
# every membership change (catastrophic for cache hit rate)

Replication & Failover

Memcached has no built-in replication or failover — a dead node loses its data and clients reroute. To add redundancy you either replicate in the client (write to N nodes, more cost), run a proxy like mcrouter/twemproxy, or accept the loss and rebuild from the origin. The idiomatic choice is the last: treat the cache as disposable, size the pool so one node's loss is tolerable, and let misses refill from the DB.

memcached
# memcached has NO built-in replication or failover.
# if a node dies, its data is gone — clients reroute to the ring.
#
# to ADD replication, options:
#
# 1) client-side replication: write to N servers per key
#    - the client writes to primary + replica(s) on each set
#    - reads try primary, fall back to replica
#    - trade-off: N x write traffic + memory, weaker consistency
#
# 2) repcached: a memcached fork with master/replica replication
#    - largely unmaintained today; avoid for new projects
#
# 3) use a layer above: twemproxy / mcrouter / mc-router
#    - proxy that handles sharding, pooling, failover
#
# 4) accept the loss: cache is volatile by design — rebuild from DB

# the "memcached way" is usually option 4: don't replicate, just
# size the pool so a single node loss is survivable
12

Memory Management (Slabs / LRU)

Slab Allocator

Memcached uses a slab allocator: memory is divided into classes of fixed-size chunks, and an item is stored in the smallest chunk that fits (rounded up). This avoids malloc/free fragmentation and gives O(1) allocation, at the cost of internal fragmentation (wasted space inside chunks) and class calcification (memory can't move between classes). It's why an even mix of value sizes matters for efficiency.

memcached
# memcached does NOT use malloc/free per item (that fragments).
# instead it pre-allocates fixed-size "slab classes" of chunks.
#
#   slab class 1: chunks of 96 bytes
#   slab class 2: chunks of 120 bytes
#   slab class 3: chunks of 152 bytes   (grow by -f factor)
#   ...
#
# a 50-byte value -> rounded up to 96-byte chunk (class 1)
# a 100-byte value -> rounded up to 120-byte chunk (class 2)
#
# trade-offs:
#   + no per-item malloc overhead, no heap fragmentation
#   + O(1) alloc/free
#   - internal fragmentation (wasted bytes inside a chunk)
#   - memory is stuck per class (can't move between classes)

# inspect with:
stats slabs

Slab Classes & Growth Factor

Slab class sizes start at the -n minimum (default 48 bytes) and grow by the -f factor (default 1.25) up to the -I max item size. Lower -f yields more, finer-grained classes (less waste, more class-calcification risk); higher -f yields fewer, coarser classes (more waste). Inspect your real value-size histogram with stats sizes and tune -f so the popular sizes land on well-filled classes.

memcached
# slab class sizes start at -n (default 48 bytes) and grow by -f:
#
#   class 1:  48 bytes  (base, -n)
#   class 2:  48 * 1.25 = 60
#   class 3:  60 * 1.25 = 75 -> 80
#   class 4:  80 * 1.25 = 100
#   ...
#   up to the max item size (-I, default 1MB)
#
# -f default 1.25 balances class count vs. waste.
#   lower (1.1) -> more classes, less waste, but more stuck memory
#   higher (1.5) -> fewer classes, more waste per chunk
#
# tune -f to your value-size distribution (check stats sizes)

# example: many small JSON values -> -f 1.1 -n 80
memcached -m 512 -f 1.1 -n 80

LRU Eviction

Eviction is per-slab-class and strictly LRU within that class: when a class is full, the least-recently-used item in that class is dropped to make room. A class with free chunks never evicts, so an unbalanced size distribution can cause heavy eviction in one class while another sits idle — the classic slab-calcification problem. Watch evictions and evicted_active in stats to spot it.

memcached
# when a slab class is full and a new item arrives in that class,
# memcached evicts the LEAST RECENTLY USED item in that class.
#
# eviction is PER-SLAB-CLASS, not global:
#   - a full class-1 (96B) evicts class-1 items only
#   - a class with free chunks never evicts
#
# this means an unbalanced workload can evict from one class
# while another class sits idle with free chunks.
#
# check eviction pressure:
stats
# STAT evictions 1234     # total items ever evicted
# STAT evicted_active 12  # evicted even though recently used (1.6+)
# STAT evicted_unfetched 50  # evicted before ever read

stats items
# STAT items:1:evicted 500   # evictions in slab class 1
# STAT items:1:outofmemory 0

Memory Limits (-m)

-m caps the item memory in MB; once hit, Memcached stops adding slab pages and evicts LRU within classes. -M changes the policy to error-on-full instead of evicting (rarely desirable for a cache). cache_memlimit resizes the cap live. Size -m to your working set with headroom — running at 100% means constant evictions and a churny, low-hit-rate cache.

memcached
# -m sets the max memory for items (in MB)
memcached -m 64        # default: 64 MB
memcached -m 4096      # 4 GB

# once -m is reached, memcached stops allocating new slab pages and
# relies on LRU eviction within each class.
#
# -M flips behavior: instead of evicting, return an ERROR on writes
# when memory is full (rarely what you want for a cache).
memcached -m 512 -M

# resize the memory cap at runtime (bytes):
cache_memlimit 2147483648   # 2 GB
# -> OK   (only grows/shrinks the page pool; doesn't drop items
#          unless shrinking forces eviction)

# monitor:
stats
# STAT bytes 424463360        # current bytes in use
# STAT limit_maxbytes 536870912  # the -m limit

Eviction Policies & Tuning

Memcached's only eviction policy is per-slab LRU — there's no LFU or random option. To reduce evictions: raise -m, shorten TTLs, rebalance value sizes, or tune -f. Modern builds add an LRU crawler and the ability to reassign slab pages (slabs reassign / slabs automove) to mitigate calcification. Detect a hot class with stats items' evicted/outofmemory counters.

memcached
# memcached's only eviction policy is LRU (per slab class).
# (Redis offers LRU/LFU/random/TTL; memcached does not.)
#
# 1.6+ adds an LRU crawler + "temporary" vs "permanent" LRU tiers:
#   lru_crawler enable
#   lru_crawler tocrawl 1000
#
# to reduce evictions:
#   - increase -m (more memory)
#   - shorten TTLs so items self-expire before LRU drops them
#   - rebalance value sizes (avoid one hot slab class)
#   - tune -f so popular sizes map to well-populated classes
#
# detect a hot/evicting class:
stats items
# STAT items:5:evicted 9999     <- class 5 is under pressure
# STAT items:5:outofmemory 0

# rebalance slab memory automatically (1.5+):
slabs reassign 5 1   # move a page from class 5 to class 1
slabs automove 1     # let the server auto-rebalance (cautious mode)

Slab Automove & Reassign

slabs reassign (manual) and slabs automove (1.5+, automatic) let you rebalance slab pages from a class with free chunks to one under eviction pressure — the cure for slab calcification. automove=1 is a cautious, slow background mover that's safe for production; =2 is aggressive and experimental. Always verify with stats slabs before and after — a wrong reassign can starve a class.

memcached
# 1.5+ lets you move a slab PAGE from a class with free chunks
# to a class under memory pressure.
#
# manual one-shot move:
slabs reassign 1 5     # take a page from class 1, give to class 5
# -> DONE  (or BUSY if the page is being freed)

# automatic rebalancing (runs in the background):
slabs automove 0       # off (default)
slabs automove 1       # cautious auto-move (slow, safe)
slabs automove 2       # aggressive (experimental; watch carefully)

# the automover watches evictions/free chunks and slowly shifts
# pages to relieve pressure. It's conservative by design.

# check current slab state:
stats slabs
# STAT 1:total_pages 4
# STAT 1:free_chunks 0    <- class 1 is full
# STAT 5:total_pages 12
# STAT 5:free_chunks 8000 <- class 5 has room to give
13

Performance Optimization

noreply & Pipelining

noreply cuts the per-write response, and pipelining batches many commands into one network round-trip — together they're the biggest throughput lever for bulk writes. Multi-get does the same for reads. The cost of noreply is losing per-command error feedback, so reserve it for non-critical, high-volume writes and keep responses on for anything where a failure must be surfaced.

memcached
# noreply skips the server's response for write commands.
# use it for fire-and-forget writes to cut latency & network:
set k1 0 0 5 noreply
hello
set k2 0 0 5 noreply
world
# (no STORED echoed back)

# pipelining: send many commands in one batch without waiting for
# each response (binary protocol and most clients support this).
# ideal for bulk inserts/refreshes.

# trade-off:
#   + fewer round-trips, higher throughput
#   - no error feedback per command (use sparingly for critical writes)

# multi-get is the read-side equivalent: one request, N keys
get user:1 user:2 user:3 user:4

Binary Protocol

The binary protocol is more efficient than ASCII — a fixed 24-byte header instead of text parsing, native quiet (noreply-style) operations for pipelining, and SASL support for auth. For small values and high-throughput bulk writes it's measurably faster. The server speaks both protocols on the same port, so enabling it is purely a client-side choice. Prefer it in production clients.

memcached
# the binary protocol (vs ASCII text) is more efficient:
#   - fixed 24-byte header (no string parsing)
#   - supports SASL authentication
#   - supports pipelining and quiet (noreply-equivalent) ops natively
#   - smaller wire footprint for small values
#
# enable on the client side (the server speaks both on the same port):
#   pymemcache: Client(..., allow_unicode_keys=True, default_noreply=True)
#   spymemcached: use the binary connection factory
#   libmemcached: MEMCACHED_BINARY_PROTOCOL

# binary quiet ops (Noop/SetQ/AddQ/...):
#   pipelined writes that only reply on error — perfect for bulk loads

# the server listens for both protocols on 11211 by default;
# no server-side flag is required

Connection Pooling

Connection pooling is essential — opening a TCP connection per request burns ephemeral ports and adds latency. Keep a small pool of persistent connections per server per process, sized to your concurrency. Watch total_connections vs curr_connections in stats: if total grows with traffic while curr stays flat, you're pooling; if total explodes, you're not. Most clients pool internally when configured.

memcached
# opening a TCP connection per request is extremely wasteful.
# reuse connections via a pool:
#
#   - keep N persistent connections per server per app process
#   - checkout -> use -> checkin (don't close)
#   - size the pool to your concurrency, not your request rate
#
# pymemcache: HashClient manages a pool internally
# Node.js memcached: set poolSize
# PHP: persistent connections via memcached::OPT_CONNECT_TIMEOUT
# Java spymemcached: one MemcachedClient (thread-safe, NIO)
#
# signs of a missing pool:
#   - high total_connections in stats (grows with traffic)
#   - TIME_WAIT exhaustion on the server (ephemeral ports)
#   - request latency dominated by connect() not get()/set()

stats
# STAT total_connections 1000000   <- too high; pool your conns
# STAT curr_connections 50         <- healthy pooled count

Bulk Operations (Multi-Get)

A multi-get fetches N keys in one round-trip — the single biggest read optimization available. Clients fan the keys out across servers in parallel, so latency is bounded by the slowest server rather than the key count. Chunk very large multi-gets (e.g. 100 keys) so no single server thread is monopolized, and remember missing keys are silently absent from the result map.

memcached
# fetching N keys in one get is far cheaper than N single gets:
#   1 network round-trip instead of N
#   server parses 1 command instead of N

# text protocol: space-separated keys
get user:1 user:2 user:3 user:4 user:5

# clients wrap this:
#   pymemcache:  mc.get_many([...])
#   Node.js:     mc.getMulti([...])
#   PHP:         $mc->getMulti([...])
#   Go:          mc.GetMulti([]string{...})

# best practices:
#   - chunk very large multi-gets (e.g. 100 keys at a time)
#     so no single server thread is monopolized
#   - keys in a multi-get fan out to multiple servers — the client
#     parallelizes, so a multi-get is bound by the SLOWEST server
#   - missing keys are simply absent from the result

Right-Sizing Values

Because items round up to the next chunk size, value size directly drives internal fragmentation — a 60-byte value in a 96-byte chunk wastes 37%. Right-size values: trim JSON whitespace, compress anything over ~1KB (and use a flags bit to mark it), and cache projections rather than whole blobs. Inspect your size histogram with stats sizes and tune -f so common sizes land near chunk boundaries.

memcached
# because of the slab allocator, value size drives memory waste:
#
#   a 60-byte value -> 96-byte chunk -> 36 bytes wasted (37%!)
#   a 97-byte value -> 120-byte chunk -> 23 bytes wasted (19%)
#
# right-size values to fit chunk boundaries:
#   - trim/normalize JSON before caching (drop whitespace, short keys)
#   - compress large values (>1KB) with gzip/snappy; set a flags bit
#   - avoid caching huge blobs; cache projections instead
#
# check your size distribution:
stats sizes      # (off-peak; it locks)

# pick -f so common sizes land near chunk boundaries:
#   if most values are 100-200 bytes, -f 1.25 is fine
#   if values cluster at a few sizes, tune -f to match

# flags bit to mark compression:
set bigval 2 0 4096   # flags=2 means "gzip compressed" (your convention)

Avoiding Hot Keys

A hot key is a single-server, single-thread bottleneck — its reads serialize on one node while others idle. Shard it into N buckets (read a random bucket, write to all on update), add a 1-second client-side TTL to coalesce reloads, or move it into an in-process LRU. Detect hot keys by per-server CPU imbalance or, on 1.6+, lru_crawler metadump to peek at access frequency.

memcached
# a single hot key creates a bottleneck: it lives on ONE server
# and ONE worker thread, so concurrent reads serialize.
#
# symptoms: one memcached server at 100% CPU while others idle;
#           one slab class churning while others are calm.
#
# fixes:
#   1) shard the hot key into N buckets:
#        key = "hot:" + str(random.randint(0, 9))   # 10 buckets
#      write to all N on update; read from a random one
#   2) add a short client-side TTL (e.g. 1s) to coalesce reloads
#   3) precompute and rotate (cache the rendered page, not the query)
#   4) move the hot value closer to the app (in-process LRU)
#
# detect with:
#   - per-server CPU/stats imbalance
#   - lru_crawler metadump all  (peek at access frequency, 1.6+)
14

Monitoring

Core Stats to Watch

The four headline metrics are hit rate (get_hits/cmd_get, target >95%), evictions (memory pressure, target 0 steady-state), memory utilization (bytes/limit_maxbytes, target <90%), and listen_disabled_num (connection cap hits, target 0). Track them as rates over time, not just counters. A falling hit rate or rising evictions are the earliest signs of an unhealthy cache.

memcached
# the four numbers to monitor continuously:
stats
# STAT cmd_get, cmd_set        # throughput (ops/sec deltas)
# STAT get_hits, get_misses    # hit rate = hits / cmd_get
# STAT evictions               # memory pressure (rising = bad)
# STAT bytes / limit_maxbytes  # memory utilization %

# derived metrics:
#   hit_rate    = get_hits / cmd_get            # target > 95%
#   eviction_r  = evictions_delta / cmd_set_delta
#   mem_usage   = bytes / limit_maxbytes        # target < 90%

# connection health:
# STAT curr_connections         # active conns
# STAT listen_disabled_num      # >0 = hit -c limit (refused conns)

# alert thresholds (suggestions):
#   hit_rate < 90%        -> investigate cold keys / undersizing
#   evictions > 0 (steady)-> add memory or shorten TTLs
#   listen_disabled_num > 0 -> raise -c

memcached-tool

memcached-tool is the bundled CLI for a quick ad-hoc check: stats for a per-slab table, display for a live-refreshing view, dump to peek at keys (debug only — it walks the LRU). It's perfect for an SSH one-off but not for continuous monitoring; pair it with a Prometheus exporter and Grafana for production dashboards and alerting.

memcached
# memcached-tool ships with the server — quick CLI stats:
memcached-tool host:port stats
memcached-tool host:port display      # live-refreshing table
memcached-tool host:port dump         # dump keys (debug only!)

# sample output of "stats":
#   #  Item_Size  Max_age  Pages  Count  Full?  Evicted Evict_Time
#   1     96B        3600s    1    100    no        0       0
#   2    120B        7200s    2    250    no        0       0
#   ...

# great for a one-off SSH check; not for continuous monitoring.
# for the latter, use a Prometheus exporter + Grafana.

Telnet Monitoring

Telnet or netcat to port 11211 gives instant interactive access to the text protocol for ad-hoc checks: stats, stats settings, stats items, stats slabs, version. Use nc one-liners for scripts (echo 'stats' | nc -q 1 host 11211). Remember flush_all is destructive — never alias it casually. This is the lowest-friction way to inspect a node.

memcached
# quick interactive check via telnet/nc:
telnet localhost 11211
stats
stats settings
stats items
stats slabs
version
quit

# or one-liners with nc:
echo "stats" | nc -q 1 localhost 11211
echo "stats slabs" | nc -q 1 localhost 11211

# flush all keys (DESTRUCTIVE — use with care):
echo "flush_all" | nc -q 1 localhost 11211

# tip: pipe commands from a file for repeatable checks
nc localhost 11211 < checks.txt

Prometheus Exporter & Grafana

memcached_exporter is the standard Prometheus integration — run one beside each node and scrape :9150. Grafana dashboards then track hit rate, eviction rate, memory usage, and per-slab fill. Alert on hit-rate drops below 90%, any steady evictions, memory over 90%, and listen_disabled_num > 0. This gives you continuous, historical visibility that ad-hoc telnet can't.

memcached
# popular exporters scrape memcached stats into Prometheus:
#
# 1) memcached_exporter (official):
#    https://github.com/prometheus/memcached_exporter
#    run alongside each memcached instance:
memcached_exporter --memcached.address=localhost:11211 \
  --web.listen-address=:9150

# 2) scrape config in prometheus.yml:
#    - job_name: 'memcached'
#      static_configs:
#        - targets: ['mc1:9150','mc2:9150','mc3:9150']

# 3) Grafana dashboard (e.g. community id 37 or custom):
#    - hit rate (get_hits / cmd_get)
#    - eviction rate (rate(evictions[5m]))
#    - memory usage (bytes / limit_maxbytes)
#    - connections (curr_connections, listen_disabled_num)
#    - per-slab fill / evictions

# alert on: hit rate < 90%, evictions > 0 steady, mem > 90%

Key Metrics & Alerting

Alert on hit-rate degradation (warning <90%, critical <75%), any steady evictions, memory over 90%, and any refused connections. Always correlate cache metrics with origin metrics — a hit-rate drop paired with a DB CPU spike is the signature of a cache-miss storm, and rising evictions paired with rising origin latency point to an undersized cache. Track uptime resets to catch silent restarts.

memcached
# metric                  alert when                     severity
# ------------------------------------------------------------------
# hit_rate                < 90% sustained                 warning
# hit_rate                < 75% sustained                 critical
# evictions (rate)        > 0 steady                      warning
# evictions (rate)        > 100/min                       critical
# mem_usage               > 90%                           warning
# mem_usage               > 98%                           critical
# listen_disabled_num     > 0 (refused conns)             critical
# curr_connections        > 80% of -c                     warning
# uptime                  resets (process restarted)      info
# cmd_get/cmd_set rates   sudden drop (app -> cache down) critical

# pair cache metrics with origin metrics:
#   if cache hit rate drops AND DB CPU spikes -> cache miss storm
#   if cache evictions rise AND origin latency rises -> undersized
15

Security

Network Isolation

Memcached's text protocol has no authentication, so it must never be exposed publicly — bind to 127.0.0.1 or a private NIC with -l, disable UDP with -U 0 if unused, and firewall 11211 to app subnets only. A publicly reachable Memcached is both an open data cache and a UDP-amplification DDoS vector (CVE-2018-1000115). The default listen address is all interfaces — always override it.

memcached
# memcached has NO authentication in the text protocol.
# it MUST NOT be exposed to the public internet.
#
# bind to loopback or a private network only:
memcached -l 127.0.0.1                  # local only
memcached -l 10.0.0.5                   # private NIC
memcached -l 10.0.0.5 -U 0              # also disable UDP

# firewall: deny 11211 from outside, allow only app subnets
# iptables example:
#   iptables -A INPUT -p tcp --dport 11211 -s 10.0.0.0/24 -j ACCEPT
#   iptables -A INPUT -p tcp --dport 11211 -j DROP

# CRITICAL: the default -l is ALL interfaces.
# A public memcached is an open cache AND a UDP amplification
# vector (CVE-2018-1000115 etc.) — disable UDP if unused.

SASL Authentication (Binary Protocol)

SASL authentication is available only on the binary protocol (start the server with -S). It uses PLAIN credentials over a connection's setup phase, adding one round-trip that pooling amortizes. Use SASL on shared or multi-tenant networks where network isolation alone is insufficient. The text protocol cannot authenticate — if you need auth, you must use the binary protocol end-to-end.

memcached
# the BINARY protocol supports SASL (PLAIN) authentication.
# the text protocol does NOT.
#
# server: start with SASL enabled (built-in in most distros)
memcached -S -l 10.0.0.5

# create a SASL user (e.g. via saslpasswd2):
echo -n "myuser" | saslpasswd2 -p -a memcached myuser

# client: provide credentials
#   pymemcache: Client(..., sasl_credentials=("myuser","pass"))
#   spymemcached: use the binary factory + auth descriptor
#   libmemcached: MEMCACHED_BEHAVIOR_SASL

# note: SASL adds a round-trip at connection setup; pooled
# connections amortize this. Use it on shared/multi-tenant networks
# where -l alone isn't enough.

Encryption & TLS

Memcached has no native TLS — to encrypt traffic, terminate TLS in a sidecar like stunnel or HAProxy in front of each node, then run plain memcached behind it on loopback. For most LAN deployments, network isolation plus optional SASL is sufficient; reserve TLS for cross-network flows or compliance-required environments. Newer forks (and some cloud managed services) add native TLS.

memcached
# memcached itself does NOT support TLS/SSL natively.
# to encrypt traffic, terminate TLS in front of the server:
#
#   app --TLS--> stunnel/haproxy --plain--> memcached
#
# stunnel example (server side):
#   [memcached]
#   accept = 11443
#   connect = 127.0.0.1:11211
#   cert = /etc/stunnel/mc.pem
#
# the client also needs a stunnel/haproxy side, OR a client that
# speaks TLS (some newer forks/patches add native TLS).
#
# for most LAN deployments, network isolation + SASL is enough;
# reserve TLS for cross-network or compliance-required flows.

Firewall & Access Control

Layer your defenses: bind to a private NIC, firewall to app subnets, disable UDP, add SASL on shared networks, run as a non-root user, and apply systemd hardening (NoNewPrivileges, ProtectSystem, PrivateTmp). Critically, never store raw secrets in Memcached — it's a cache, not a vault; encrypt-then-cache or keep secrets in a dedicated secrets manager and cache only non-sensitive references.

memcached
# defense in depth for memcached:
#
# 1) bind to a private interface (-l 10.0.0.5)
# 2) firewall 11211 to app subnets only
# 3) disable UDP if unused (-U 0) — UDP is an amplification vector
# 4) use SASL on shared networks
# 5) TLS via stunnel for cross-network
# 6) run as a non-root user (-u memcache)
# 7) drop privileges / use systemd hardening
# 8) never store secrets in memcached unencrypted — it's a cache,
#    not a vault (use a secrets manager + encrypt-then-cache)

# systemd hardening example:
# [Service]
#   User=memcache
#   NoNewPrivileges=true
#   ProtectSystem=strict
#   PrivateTmp=true

Security Best Practices

The cardinal rules: never expose Memcached publicly, always bind to a private interface, disable UDP, and use SASL on shared networks. Don't store secrets or PII unencrypted; namespace keys per tenant to prevent leakage across users. Keep the server patched, audit connections with stats conns, and treat a cache breach as a data breach — rotate any tokens that may have been cached.

memcached
# 1) NEVER expose memcached to the internet (no auth in text proto)
# 2) ALWAYS set -l to loopback or a private address
# 3) disable UDP (-U 0) unless you specifically need it
# 4) use SASL on the binary protocol for multi-tenant networks
# 5) don't store secrets/PII in the cache unencrypted
# 6) namespace keys per-tenant to avoid cross-tenant leakage
#    (e.g. "tnt:42:user:1" — never shared global keys)
# 7) keep memcached updated (security CVEs do happen)
# 8) audit with stats conns to see who's connected
# 9) log and alert on unexpected connection sources
# 10) treat a cache breach as a data breach: rotate any tokens that
#     might have been cached
16

Memcached vs Redis

Data Types & Features

Memcached is a pure string key-value cache; Redis is a data-structure server with lists, hashes, sets, sorted sets, streams, Lua scripting, transactions, pub/sub, and modules. Redis's richness is a strength when you need it, but Memcached's minimalism is a feature for pure caching — less surface area, less to misconfigure. Choose by whether you need server-side data structures.

memcached
# Memcached:
#   - strings only (key -> bytes), plus the numeric incr/decr trick
#   - no lists, hashes, sets, sorted sets, streams, pub/sub
#   - no server-side scripting (no Lua)
#   - no transactions (single-key CAS only)
#   - no keyspace notifications
#
# Redis:
#   - rich types: strings, lists, hashes, sets, zsets, streams, ...
#   - Lua scripting, MULTI/EXEC transactions, pub/sub
#   - modules (RedisJSON, RediSearch, RedisGraph, ...)
#   - per-key TTL on any type
#
# verdict: Redis is a data-structure server; memcached is a
# pure key-value cache. Pick memcached when you only need a cache.

Persistence

Memcached has zero persistence — a restart empties it by design, and the cache is always disposable. Redis offers RDB snapshots and AOF durability plus replication, so it can serve as a primary datastore. If you need the cache to survive restarts or hold authoritative data, Redis wins; if you want a pure volatile cache that rebuilds on miss, Memcached's simplicity is a virtue.

memcached
# Memcached:
#   - NO persistence. A restart loses everything. By design.
#   - no snapshots, no AOF, no replication
#   - the cache is always disposable; rebuild from the DB on miss
#
# Redis:
#   - RDB snapshots (point-in-time) and AOF (append-only log)
#   - can act as a primary datastore with durability
#   - replication + sentinel/cluster for HA
#
# verdict: if you need the cache to survive restarts, use Redis.
# if you want a pure volatile cache (and rebuild on miss), memcached's
# lack of persistence is simpler and faster.

Replication & Clustering

Memcached has no server-side clustering or replication — distribution is entirely client-side, and a dead node permanently loses its keys. Redis offers master/replica replication, Sentinel for HA, and Redis Cluster for sharding with automatic failover. Memcached is simpler to operate (no gossip, no failover logic) but offers no HA; Redis trades operational complexity for true resilience.

memcached
# Memcached:
#   - NO server-side clustering or replication
#   - sharding is 100% client-side (consistent hashing)
#   - a dead node loses its keys; clients reroute
#   - scale out = add nodes to the ring
#
# Redis:
#   - master/replica replication (async)
#   - Sentinel for HA/failover
#   - Redis Cluster for sharding + automatic failover
#   - more moving parts, but true HA
#
# verdict: memcached is simpler to operate (no cluster gossip) but
# has no failover — a dead node is data loss for its keys.
# Redis gives you replication + failover at the cost of complexity.

Performance Characteristics

Both are in-memory and sub-millisecond. Memcached is multi-threaded by default and scales across cores for pure get/set, while Redis is (mostly) single-threaded for command execution — so on a big multi-core box doing only get/set, Memcached can hit higher QPS. For most real apps the difference is negligible; choose based on features and operational fit, not microbenchmarks.

memcached
# Both are in-memory and very fast (sub-millisecond, >100k ops/s).
#
# Memcached:
#   - multi-threaded by default (-t workers); scales across cores
#   - simple protocol, low overhead per op
#   - excellent for high-throughput pure get/set
#
# Redis:
#   - single-threaded command execution (Redis 6+ threads the I/O)
#   - one core does the work; more cores don't speed a single command
#   - richer ops can be slower, but core get/set is comparable
#
# verdict: for pure get/set at very high QPS on a big multi-core box,
# memcached's multi-threading can win. For most apps the difference
# is negligible; pick by features, not microbenchmarks.

Memory Efficiency

Memcached's slab allocator is lean for opaque blobs but wastes space to chunk rounding; Redis has per-key/field overhead but picks compact encodings (listpack) for small structures. Neither is universally smaller — Memcached wins for uniform blobs, Redis can win for small structured data. Always measure with your own value shapes before assuming one is cheaper.

memcached
# Memcached:
#   - slab allocator: fixed chunks, some internal fragmentation
#   - no per-field overhead (it's just bytes)
#   - very lean for small uniform values
#   - can waste memory if value sizes don't fit slab classes
#
# Redis:
#   - per-key/per-field overhead (complex internal structures)
#   - many encodings (listpack, hashtable, skiplist, ...) chosen by size
#   - richer structures use more memory than a flat string
#   - but more memory-efficient for small structured data via listpack
#
# verdict: memcached is leaner for opaque blobs; Redis can be leaner
# for small structured data (listpack). Measure with your own data.

When to Choose Which

Choose Memcached for a pure, disposable get/set cache where operational simplicity and multi-threaded scaling matter. Choose Redis when you need data structures, persistence, replication, clustering, Lua, pub/sub, or modules. They're not mutually exclusive — many stacks run both, using Memcached for raw page/query caching and Redis for sessions, leaderboards, rate-limiting, and streams.

memcached
# Choose Memcached when:
#   - you need a pure, disposable get/set cache
#   - you value operational simplicity (no clustering, no persistence)
#   - you want multi-threaded scaling on a big box
#   - your data is opaque blobs and you rebuild on miss
#
# Choose Redis when:
#   - you need data structures (lists, hashes, sets, zsets, streams)
#   - you need persistence or durability
#   - you need replication / failover / clustering
#   - you need server-side logic (Lua, pub/sub, modules)
#   - you need per-key TTL on rich types
#
# both can coexist: memcached for raw page/query cache, Redis for
# sessions, leaderboards, rate-limiting, streams.
17

Troubleshooting

Connection Refused

Connection-refused usually means the server isn't running, isn't listening on the expected address (a loopback-bound -l blocks remote clients), is firewalled, or has hit the -c connection cap (listen_disabled_num > 0). Walk the checklist: ps/systemctl for the process, ss/netstat for the listener, iptables for the firewall, stats for the connection cap, and journalctl for crash logs.

memcached
# symptom: client reports "connection refused" to 11211
#
# 1) is memcached running?
ps aux | grep memcached
systemctl status memcached

# 2) is it listening on the expected address/port?
ss -tlnp | grep 11211
netstat -tlnp | grep 11211

# 3) check the -l bind address (default = ALL; you may have set 127.0.0.1)
#    a client on another host can't reach a loopback-bound server

# 4) firewall blocking?
iptables -L -n | grep 11211

# 5) hit the -c connection limit? (check listen_disabled_num)
echo "stats" | nc -q 1 host 11211 | grep listen_disabled

# 6) check memcached logs / journalctl
journalctl -u memcached -n 100

Eviction Storms

Rising evictions with a full cache mean your working set exceeds -m. Diagnose with stats (bytes == limit_maxbytes) and stats items (which slab class is evicting). Fixes: raise -m, shorten TTLs so items self-expire, rebalance slab pages with slabs reassign/automove, and hunt for a runaway writer bloating values. A persistently full cache needs more memory or a smaller working set.

memcached
# symptom: evictions counter rising fast, hit rate dropping
#
# diagnosis:
stats
# STAT evictions 999999     <- climbing
# STAT bytes 67108864
# STAT limit_maxbytes 67108864   <- 100% full

stats items
# STAT items:3:evicted 500000    <- slab class 3 is the victim
# STAT items:3:outofmemory 0

# fixes:
# 1) add memory: raise -m (or cache_memlimit at runtime)
# 2) shorten TTLs so items self-expire before LRU drops them
# 3) rebalance slab classes: slabs reassign <donor> <receiver>
#    or: slabs automove 1
# 4) check for a runaway writer creating oversized values
# 5) review your working set — maybe you're caching too much

Low Hit Rate

A low hit rate (<90%) with high DB load usually means TTLs are too short, the working set exceeds memory (evictions), keys aren't being reused (volatile components like timestamps in the key), it's a cold start, or clients disagree on ketama config so keys land on different servers. Verify all clients share identical server lists and hashing, and check your app logs for key reuse patterns.

memcached
# symptom: hit rate < 90%, DB load high
#
# diagnosis:
stats
# STAT cmd_get 100000
# STAT get_hits 70000          <- 70% hit rate, too low
# STAT get_misses 30000

# common causes:
# 1) TTL too short -> items expire before reuse (raise TTL)
# 2) working set > cache memory -> evictions (see eviction storm)
# 3) keys not reused -> each request computes a unique key
#    (e.g. including a timestamp; remove volatile components)
# 4) cold start -> warmup period; preheat the cache
# 5) multiple client configs -> keys hashed to different servers
#    (verify all clients use the same ketama config)
# 6) cache invalidated too eagerly on writes (delete too broad)

# check TTL distribution vs access patterns in your app logs

Slab Calcification

Slab calcification is when memory is stuck in classes that don't need it while another class churns through evictions — pages can't migrate on their own. Diagnose with stats slabs (free_chunks vs total_pages) and stats items (evicted per class). Fix with slabs reassign (one-shot) or slabs automove 1 (background), and prevent recurrence by tuning -f to match your value-size distribution.

memcached
# symptom: one slab class keeps evicting while another has free chunks
#
# diagnosis:
stats slabs
# STAT 1:total_pages 4    STAT 1:free_chunks 0      <- class 1 full
# STAT 5:total_pages 12   STAT 5:free_chunks 8000   <- class 5 idle
#
# stats items
# STAT items:1:evicted 99999   <- class 1 churning

# cause: over time, all pages got assigned to popular classes;
# memory is stuck and can't move to where it's needed.

# fixes:
# 1) one-shot: slabs reassign 5 1    # move a page class5 -> class1
# 2) auto:     slabs automove 1      # cautious background rebalance
# 3) restart with a better -f growth factor that matches your data
# 4) enable lru_crawler for smarter eviction

# verify after:
stats slabs

High Memory Usage

Unexpected memory usage is usually internal fragmentation (values not fitting chunks), bloated values (uncompressed JSON), or sheer key count (per-item overhead). Diagnose with stats (bytes vs limit_maxbytes) and stats slabs (used vs total chunks). Fixes: compress large values, trim whitespace and key names, tune -f to reduce waste, and verify -m matches the working set you actually need.

memcached
# symptom: memcached using more memory than expected
#
# diagnosis:
stats
# STAT bytes 600000000          # actual item bytes
# STAT limit_maxbytes 671088640 # the -m cap
# STAT bytes_read / bytes_written help gauge traffic

stats slabs
# look at used_chunks vs total_chunks per class — internal
# fragmentation = (total_chunks * chunk_size) - bytes

# common causes:
# 1) internal fragmentation (values don't fit chunks well)
# 2) bloated values (cache full JSON with whitespace; compress!)
# 3) too many keys (key+metadata overhead per item)
# 4) memory leak in the client holding dead connections
# 5) -m is just set high and the working set filled it

# fix: compress large values, trim keys, tune -f, lower -m

Slow Responses & Timeouts

Slow responses usually come from a hot key serializing on one thread, an oversized multi-get blocking the server, huge values saturating the network, app-to-cache network latency, CPU saturation from too few -t threads, or — critically — Memcached swapping to disk (never let it swap; pin -m below physical RAM). Set aggressive client timeouts (50–100ms) so a slow cache degrades to the origin instead of stalling requests.

memcached
# symptom: get/set taking >10ms or timing out
#
# 1) a hot key serializing on one server thread (see hot keys)
# 2) huge multi-get blocking a server thread — chunk it
# 3) oversized values (multi-MB) saturating the network/parse
# 4) network latency between app and cache (co-locate them!)
# 5) server CPU saturated (-t too low for load)
# 6) swap! memcached must NEVER swap — check free -m; pin -m below RAM
#
# diagnose:
stats
# STAT curr_connections 4000   <- near -c? pool leak?
# STAT threads 4               <- raise -t?
# top -H -p <pid>              <- which thread is hot?

# on the client: set aggressive timeouts (e.g. 50-100ms) so a slow
# cache degrades gracefully to the origin instead of stalling
18

Deployment Configuration

Running as a Daemon

Run Memcached as a daemon with -d, always dropping privileges with -u to a non-root user. The essential production flags are -m (memory), -l (bind address — never leave it on all interfaces), -c (connection cap), -t (threads), and -f (slab growth factor). Write a pidfile with -P for easy management, and verify the process with a quick version check over netcat.

memcached
# start memcached as a background daemon (-d):
memcached -d -m 256 -p 11211 -u memcache -l 127.0.0.1 -c 2048

# common production flags:
#   -d              daemonize
#   -u memcache     drop privileges to this user
#   -m 256          256 MB item memory
#   -l 127.0.0.1    bind to loopback (or private NIC)
#   -c 2048         max connections
#   -t 8            8 worker threads
#   -f 1.25         slab growth factor
#   -I 1m           max item size
#   -v / -vv        log verbosity
#   -P /var/run/memcached.pid   write a pidfile

# check it's up:
echo "version" | nc -q 1 127.0.0.1 11211
# -> VERSION 1.6.24

Systemd Service

A systemd unit gives you auto-restart (Restart=on-failure), privilege dropping (User=memcache), file-descriptor limits (LimitNOFILE for many connections), and security hardening (NoNewPrivileges, ProtectSystem, PrivateTmp). Most distros ship a default unit you can override via /etc/systemd/system/memcached.service.d/override.conf rather than rewriting the whole file — prefer that for portability.

memcached
# /etc/systemd/system/memcached.service
[Unit]
Description=Memcached
After=network.target

[Service]
Type=simple
User=memcache
Group=memcache
ExecStart=/usr/bin/memcached -m 256 -p 11211 -l 127.0.0.1 -c 2048 -t 8 -f 1.25
Restart=on-failure
RestartSec=2
LimitNOFILE=8192

# hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/run

[Install]
WantedBy=multi-user.target

# enable and start:
#   systemctl daemon-reload
#   systemctl enable --now memcached
#   systemctl status memcached

Docker Deployment

In Docker, remember there are two memory limits: memcached's -m (item memory) and the container's memory cap (docker -m). Always set the container cap higher than memcached's -m to leave room for the runtime and connections, or the OOM killer will strike. Pin a specific version tag (memcached:1.6) rather than :latest for reproducibility, and restart=unless-stopped for resilience.

memcached
# run memcached in Docker:
docker run -d \
  --name memcached \
  -p 11211:11211 \
  -m 512m \
  --restart=unless-stopped \
  memcached:1.6 \
  -m 256 -c 2048 -t 4 -f 1.25

# note: -m inside the container is memcached's item memory (256 MB);
#       the docker -m 512m limit caps the whole container (memcached +
#       overhead). Always set the docker limit HIGHER than memcached's -m.

# docker-compose snippet:
#   memcached:
#     image: memcached:1.6
#     command: -m 256 -c 2048 -t 4
#     ports:
#       - "11211:11211"
#     restart: unless-stopped

Multiple Instances

Running multiple Memcached instances per host lets you isolate hot slab classes, shrink per-process restart impact, and use more cores — the client just sees them as additional ring members. The cost is more connections and per-process slab-page overhead. For most workloads a single multi-threaded process (-t to your core count) is simpler and sufficient; split only when you hit a specific bottleneck.

memcached
# run several memcached instances per host for isolation or to
# use more cores efficiently:
#
# instance A: port 11211, 4 GB
memcached -d -m 4096 -p 11211 -l 127.0.0.1 -t 4
# instance B: port 11212, 4 GB
memcached -d -m 4096 -p 11212 -l 127.0.0.1 -t 4

# the client treats them as two ring members (more shards).
# trade-offs:
#   + isolates a hot slab class to its own process
#   + smaller per-process working set = simpler restarts
#   - more connections to manage
#   - more memory overhead per process (slab pages don't share)

# alternative: one process with -t 8 threads (simpler, usually fine)

Memory Sizing

Size -m to your measured working set plus headroom, targeting 70–80% utilization at peak so you have room for spikes and don't churn evictions. Never let Memcached swap — keep -m below available RAM after accounting for the OS and other processes. If steady utilization exceeds 90%, either raise -m or shrink the working set (shorter TTLs, fewer cached things).

memcached
# how big should -m be?
#
# 1) measure your working set: the keys actually hot in a typical hour
# 2) add headroom: target ~70-80% memory utilization at peak
# 3) leave RAM for the OS, connections, and slab overhead
#
# rule of thumb:
#   -m = working_set_bytes / 0.75
#
# example: 3 GB working set -> -m 4096 (4 GB) for 75% utilization
#
# never let memcached swap:
#   -m must be BELOW available RAM (check free -m)
#   account for other processes on the same host
#
# monitor utilization:
stats
# STAT bytes 3221225472          # 3 GB used
# STAT limit_maxbytes 4294967296 # 4 GB cap  -> 75% utilization

# if utilization > 90% steady: raise -m or shrink the working set

Logging & Verbosity

Memcached is quiet by design; raise verbosity with -v/-vv/-vvv or the runtime verbosity command only for debugging, since logging every command crushes throughput. In production, run quiet and rely on stats and a Prometheus exporter for visibility. Logs go to journald under systemd or docker logs in containers. Use -vv briefly to trace a specific issue, then turn it back down.

memcached
# memcached is deliberately quiet; increase verbosity for debugging:
memcached -v        # log errors + warnings
memcached -vv       # also log each command (very noisy!)
memcached -vvv      # even more (connection-level detail)

# change at runtime without restart:
verbosity 2         # telnet in and raise the level
verbosity 0         # back to quiet

# with systemd, logs go to journald:
journalctl -u memcached -f

# with Docker:
docker logs -f memcached

# in production, run quiet (no -v) — verbose modes are only for
# debugging, as logging every command kills throughput.
# for access patterns, use stats + an exporter instead of -vv.
19

Advanced Topics

Binary Protocol Deep Dive

The binary protocol's fixed 24-byte header enables efficient parsing, SASL auth, and quiet (response-suppressing) opcodes that make pipelined bulk operations far more efficient than the text protocol. A Noop at the end of a quiet pipeline flushes it and surfaces errors. Most production clients default to or prefer the binary protocol for its lower per-op overhead and richer feature set.

memcached
# the binary protocol uses a fixed 24-byte request header:
#
#  Magic (1)    0x80 request / 0x81 response
#  Opcode (1)   0x01 Get / 0x02 Set / 0x03 Add / ... / 0x05 Delete ...
#  Key length (2)
#  Extras length (1)
#  Data type (1)   usually 0
#  VBucket/reserved (2)
#  Total body length (4)   extras + key + value
#  Opaque (4)              client-supplied, echoed back
#  CAS (8)
#
# followed by: extras | key | value
#
# "quiet" opcodes (GetQ, SetQ, ...) skip responses except on error,
# enabling efficient pipelined bulk operations.
#
# a Noop at the end flushes the pipeline and confirms success.
# prefer the binary protocol in production clients for lower overhead.

UDP Support

Memcached supports UDP for high-throughput, connectionless, fire-and-forget lookups — useful in specialized setups where TCP handshake cost dominates. It's lossy and size-limited, so only use it for idempotent cache reads. Crucially, UDP is a known amplification DDoS vector (CVE-2018-1000115); disable it with -U 0 in production unless you have a specific, firewalled need.

memcached
# memcached can serve UDP (-U port, default 11211; -U 0 disables).
# UDP is for very high-throughput, fire-and-forget scenarios where
# a connection setup is too costly.
#
# each UDP datagram carries:
#   frame header (8 bytes): request ID, sequence, total datagrams
#   then the (possibly multi-datagram) command
#
# limits:
#   - responses may arrive out of order or be lost
#   - datagram size limits (~1400 bytes payload to avoid fragmentation)
#   - no guarantees — use only for cacheable, idempotent lookups
#
# SECURITY: UDP is a known amplification vector (CVE-2018-1000115).
# DISABLE it (-U 0) unless you have a specific, firewalled use case.

memcached -U 0    # recommended in production

Large Objects & Chunking

While you can raise the 1MB item limit with -I, large items hurt throughput and slab balance. A better pattern is application-level chunking: split the value into ~512KB pieces under numbered keys, store a count key, and reassemble on read. This keeps items slab-friendly, avoids monopolizing server threads, and makes a partial failure recoverable rather than losing the whole blob.

memcached
# memcached's default max item size is 1 MB (-I).
# raising it (-I 4m) works but large items hurt: they're slow to
# transfer, monopolize server threads, and risk slab fragmentation.
#
# better: chunk large values yourself:
#
def set_large(key, value, ttl=3600):
    chunks = [value[i:i+512*1024] for i in range(0, len(value), 512*1024)]
    for idx, c in enumerate(chunks):
        mc.set(key + ":" + str(idx), c, ttl)
    mc.set(key + ":n", len(chunks), ttl)

def get_large(key):
    n = mc.get(key + ":n")
    if n is None: return None
    return b"".join(mc.get(key + ":" + str(i)) for i in range(n))

# trade-off: more keys/round-trips, but smaller items fit slabs
# better and a single chunk failure doesn't lose the whole value.

Namespacing & Versioning

Memcached has no real namespaces — fake them with key prefixes, and use a versioned prefix to invalidate a whole logical namespace at once (bump a nsver key so old keys become unreachable and age out via TTL). Including an app-version prefix gives every deploy a fresh cache. This is the canonical way to bulk-invalidate, since Memcached can't enumerate or pattern-delete keys.

memcached
# memcached has no native namespaces — fake them with key prefixes.
#
# per-tenant isolation:
key = "tnt:" + tenant_id + ":user:" + user_id

# version the whole namespace to invalidate en masse:
ns_version = mc.get("nsver:user") or "1"
key = "user:" + ns_version + ":" + user_id
# to invalidate ALL user:* keys: incr "nsver:user" -> new version
# old keys become unreachable and expire via TTL

# or include a deploy version so each deploy starts fresh:
key = "v" + APP_VERSION + ":user:" + user_id

# time-bucketed namespaces for reports:
key = "report:" + today_date()   # auto-expires by date

# NEVER try "flush_all user:*" — memcached can't enumerate keys.
# versioned prefixes are the canonical bulk-invalidation trick.

Cache Stampede Prevention

A stampede happens when many requests miss the same expiring key and all recompute it, hammering the origin. The canonical fix is an add-based lock so only one client rebuilds while others briefly wait and retry. Combine with TTL jitter to prevent synchronized expirations, and proactive refresh (refresh before expiry, serve stale while one client rebuilds) for hot keys where even a single miss is costly.

memcached
# a stampede: many requests miss the same expiring key, all
# recompute the same expensive value, overwhelming the origin.
#
# fix 1: lock-and-recompute (only ONE client rebuilds)
def get_with_lock(key, rebuild_fn, ttl=3600, lock_ttl=30):
    val = mc.get(key)
    if val is not None:
        return val
    # try to take a rebuild lock
    if mc.add("lock:" + key, "1", ttl=lock_ttl):
        try:
            val = rebuild_fn()           # only the winner recomputes
            mc.set(key, val, ttl)
            return val
        finally:
            mc.delete("lock:" + key)
    else:
        # another client is rebuilding; wait briefly and retry
        time.sleep(0.05)
        return get_with_lock(key, rebuild_fn, ttl, lock_ttl)

# fix 2: add jitter to TTLs so expirations don't synchronize
ttl = base_ttl + random.randint(-60, 60)

# fix 3: "dogpile" — serve a stale value while one client rebuilds
# (store value + a "refresh_at" timestamp; refresh proactively)

Was this helpful?