Memcached CAS Commands: Preventing Lost Updates Without Distributed Locks
Memcached's `gets` and `cas` commands provide optimistic concurrency control for read-modify-write operations. Learn how they prevent lost updates, where they fall short, and how to verify the behavior locally.
28 Nov 2025, 07:20 UTC

The Lost-Update Problem in a Cache
Two workers pull the same JSON blob from Memcached, each applies a local edit, and each writes the result back. The second write silently clobbers the first. This is the lost-update problem: a race condition that appears whenever multiple clients perform a read-modify-write cycle on the same cached value.
The usual fix is a distributed lock, but locks add latency and create failure modes of their own. Memcached offers a lighter alternative through optimistic concurrency control: the gets and cas commands.
How gets and cas Work
gets behaves like get, but it returns a unique CAS identifier alongside the value. Think of this token as a version number for that specific key. When you are ready to write, you send the token back with cas. The server compares the token you supplied with the current one. If they match, it stores your value and replies STORED. If another client has touched the key in the meantime, the token has changed and the server rejects the write with EXISTS.
In the binary protocol the same idea applies, but the CAS token is a 64-bit integer and the server returns a distinct status code rather than the text strings STORED or EXISTS. The semantics are identical; only the wire format differs.
A Worked Race-Condition Example
Assume a key named config holds a small JSON object. Two application instances want to update different fields. The commands below use the text protocol over a raw TCP connection to the default Memcached port 11211. Run them from any client that can open a socket—nc, telnet, or a language-specific client.
# Connection A
gets config
VALUE config 0 45 7
{"theme":"dark","lang":"en","tz":"UTC"}
END
# Connection B (concurrently)
gets config
VALUE config 0 45 7
{"theme":"dark","lang":"en","tz":"UTC"}
END
Both connections receive the same CAS token (7). Now each tries to write back a modified value:
# Connection A
cas config 0 46 7
{"theme":"light","lang":"en","tz":"UTC"}
STORED
# Connection B
cas config 0 46 7
{"theme":"dark","lang":"fr","tz":"UTC"}
EXISTS
Connection A succeeds because its token still matches. Connection B is rejected because the server has already moved to a new token. B must re-read the value with gets, reapply its change on top of A's update, and try again. No lock was held, and no update was lost.
Expected checks: a successful cas returns STORED; a stale token returns EXISTS. If you see NOT_FOUND, the key was evicted or expired between the gets and the cas.
Limits and Trade-offs
CAS is not a lock. It does not block readers or writers, so it cannot enforce mutual exclusion around a critical section. It only guarantees that a single-key write is rejected if the value changed since you read it.
- Single-key scope. There is no multi-key transaction. Updating two keys atomically requires a different pattern.
- Token churn. The CAS identifier changes for reasons unrelated to competing writers: LRU eviction, TTL expiration, or an explicit
delete. A valid update can fail simply because the cache evicted the item. - Contention cost. Under heavy write load on one key, clients may see repeated
EXISTSresponses. Retry loops with backoff, or sharding the key across multiple entries, are common mitigations. - Cache staleness. A successful
casmeans the cache accepted your value. It does not mean the underlying database or system of record has not changed.
Verifying the Behavior Locally
You can confirm these semantics with a local Memcached instance. Start the server as a non-privileged user:
memcached -u $USER -p 11211
Open two terminal sessions and connect with nc localhost 11211. In session one, issue gets mykey. In session two, issue cas mykey 0 <bytes> <cas_id> with the token from session one. If you modify the value between the two commands, the server should reply EXISTS. Use stats to check the server version and confirm protocol support, since text and binary error semantics can vary slightly across implementations.
Finally, review your client library's API. Some libraries expose the raw CAS token; others wrap it in an optimistic-locking abstraction that hides the retry loop. If the retry loop is hidden, verify how the library surfaces contention so that repeated failures do not become silent update losses.
When to Reach for CAS
Use gets and cas when you have a single-key read-modify-write operation in a cache and can tolerate occasional retries. They remove the need for a distributed lock in the common case of low-to-moderate contention. If your workload hammers one key, or if you need atomicity across multiple keys, CAS is the wrong tool—look instead at key sharding, a queue, or a transactional data store.
Treat the cache as an optimization, not the source of truth. CAS keeps your cache coherent with itself; keeping it coherent with your database is still your application's job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.