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.
# 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 mykeyProtocol 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.
# 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.24Key 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.
# 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 4mCommon 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.
# 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 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 serverBasic 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).
# 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
# -> STOREDget — 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.
# 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.
# 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
# -> STOREDdelete — 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.
# 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.
# 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_FOUNDStorage 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.
# 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
# -> 1replace — 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).
# 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.
# 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.
# 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 changeStorage 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.
# 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 responseCAS (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.
# 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.
# 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.
# 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.
# 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.
# 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)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.
# 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_FOUNDdecr — 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).
# 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_ERRORInitializing 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.
# 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-02Counter 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.
# 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 1Counter 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.
# 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 racesExpiration & 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.
# 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'.
# 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_FOUNDgat & 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.
# 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 cExpiration 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 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.
# 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 deleteStats 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.
# 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 pressurestats 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.
# 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 / hotstats 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.
# 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 slotsstats 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.
# 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.
# 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 commandstats 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.
# 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)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.
# 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.
# 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 verbosityRuntime 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.
# 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 secondsImportant 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.
# -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 throughputConnection 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.
# -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 connsCaching 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.
# 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 invalidatedRead-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.
# 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.
# 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.
# 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.
# 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 insteadTTL & 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.
# 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 shortlyClient 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.
# 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.
// 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 shutdownPHP (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.
<?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.
# 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.