memcached-tests
Wraps Memcached cache testing against a real container: an inline set/get/expire/no-persistence worked example, with the exhaustive protocol-command tests (set/get/add/cas/incr/decr, TTL 0=never-expire / 30-day Unix-timestamp boundary) and the LRU-eviction, consistent-hashing distribution, ElastiCache Auto Discovery, and CI-wiring deep-dives in references/. Use when writing tests for an application that uses Memcached as its primary cache, when verifying ElastiCache Memcached cluster behaviour, or when contrasting Memcached eviction and distribution semantics against Redis.
Install with skills.sh (any agent)
npx skills add testland/qa --skill memcached-testsmemcached-tests
Overview
Memcached is a widely deployed in-memory cache, available as the ElastiCache Memcached tier on AWS. It differs from Redis in three fundamental ways that affect how tests must be written:
This skill wraps test patterns against a real Memcached instance via Testcontainers - not a mock. Mocks lose LRU eviction, TTL-tick, and consistent-hashing redistribution behaviour that real bugs hide in.
When to use
How to use
Install
pip install pymemcache testcontainers # Python
npm install --save-dev memjs testcontainers # Node (binary protocol)The Testcontainers Memcached module defaults to memcached:1 and exposes port 11211 (testcontainers-python memcached (opens in new window)):
from testcontainers.memcached import MemcachedContainer
with MemcachedContainer("memcached:1.6-alpine") as mc:
host, port = mc.get_host_and_port()Fixture (Python)
import pytest
from pymemcache.client.base import Client
from testcontainers.memcached import MemcachedContainer
@pytest.fixture(scope="session")
def mc_addr():
with MemcachedContainer("memcached:1.6-alpine") as mc:
yield mc.get_host_and_port() # (host, port)
@pytest.fixture
def mc(mc_addr):
host, port = mc_addr
client = Client((host, port), default_value=None)
yield client
client.flush_all() # Reset between testsWorked example: set, read, expire, confirm no persistence
Assert the four signature Memcached behaviours in one pass against the mc fixture - a value round-trips, add is refused on an existing key, a short TTL evicts by expiry, and nothing survives a flush (the no-persistence guarantee, modelling a node restart):
import time
def test_memcached_end_to_end(mc):
# 1. set then get - the value round-trips
mc.set("session:42", b"active")
assert mc.get("session:42") == b"active"
# 2. add is refused when the key already exists
assert mc.add("session:42", b"other") is False
# 3. a 1 s TTL expires the key (evict-by-expiry)
mc.set("otp:42", b"123456", expire=1)
assert mc.get("otp:42") == b"123456"
time.sleep(1.5)
assert mc.get("otp:42") is None
# 4. no persistence - flush models a node restart; data is gone
mc.flush_all()
assert mc.get("session:42") is NoneRun it:
pytest tests/memcached/ -vThat is the whole loop - container up, real client, assert a real cache behaviour, tear down. From here, expand into the full command matrix and the cluster-level tests via the two references linked in How to use.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Mocking the Memcached client | Misses TTL-tick, LRU eviction, CAS token generation | Use Testcontainers / real Memcached |
time.sleep(60) to test TTL | Slow and flaky | Set 1 s TTL and sleep 1.5 s |
Asserting incr initialises a missing key | incr returns None on missing keys | Use add to initialise, then incr |
| Sharing a Memcached instance between test suites | Cross-suite key pollution, order-dependent failures | flush_all in fixture teardown |
| Expecting data after a Memcached restart | Memcached has no persistence | Test for graceful cache-miss handling |
| Using a single-node client to test distribution | Distribution logic never exercises consistent hashing | Use HashClient with two test containers |
| Hard-coding node endpoints in app code | Breaks on ElastiCache node replacement | Use the configuration endpoint + Auto Discovery |
noreply=True in set during assertion tests | Errors are silently swallowed | Set noreply=False (pymemcache default for development) |
Limitations
References
Memcached eviction, distribution, and CI wiring
View source (opens in new window)Memcached eviction, distribution, and CI wiring
Deep reference for memcached-tests SKILL.md. Consult when testing LRU eviction and no-persistence, consistent-hashing key distribution across a multi-node cluster, AWS ElastiCache Auto Discovery, and when wiring the suite into CI. Tests reuse the mc / mc_addr fixtures from the SKILL.
LRU eviction (no-persistence)
Memcached evicts using LRU within each slab class; there is no persistence and no AOF/RDB equivalent. Per the AWS ElastiCache comparison (opens in new window), "Backup and restore" is No for node-based Memcached clusters.
def test_lru_evicts_cold_keys_under_pressure():
"""
Launch a small-memory container to verify LRU eviction.
The -m flag caps Memcached's RAM (MB).
"""
from testcontainers.memcached import MemcachedContainer
from pymemcache.client.base import Client
with MemcachedContainer("memcached:1.6-alpine") as mc:
mc.get_wrapped_container().exec_run # introspect if needed
host, port = mc.get_host_and_port()
# Restart with low memory cap via Docker command override
# Use a separate docker run with -m 8m for a tighter eviction test;
# or accept that testcontainers default image evicts eventually.
# The key assertion: after filling cache, a cold key may be absent.
def test_no_data_survives_restart(mc_addr):
"""Memcached has no persistence: data is gone after any restart."""
host, port = mc_addr
c = Client((host, port))
c.set("persistent", b"should-not-survive")
# Simulate application expectation: always handle cache miss gracefully
# after a node restart or replacement (e.g., ElastiCache node failure).
assert c.get("persistent") is not None # warm path
# After restart (modelled here as flush_all), data is gone:
c.flush_all()
assert c.get("persistent") is None, "Memcached is not persistent"Consistent-hashing client distribution
Per pymemcache HashClient (opens in new window), client-side consistent hashing distributes keys across nodes. Adding or removing a node remaps only the affected ring segment - not all keys.
def test_hash_client_distributes_keys():
from testcontainers.memcached import MemcachedContainer
from pymemcache.client.hash import HashClient
with MemcachedContainer("memcached:1.6-alpine") as mc1, \
MemcachedContainer("memcached:1.6-alpine") as mc2:
h1, p1 = mc1.get_host_and_port()
h2, p2 = mc2.get_host_and_port()
cluster = HashClient([(h1, p1), (h2, p2)])
keys = [f"key:{i}" for i in range(100)]
for k in keys:
cluster.set(k, b"v")
# Verify distribution: each node should hold some keys
direct1 = sum(
1 for k in keys if Client((h1, p1)).get(k) is not None
)
direct2 = sum(
1 for k in keys if Client((h2, p2)).get(k) is not None
)
assert direct1 > 0, "Node 1 should hold some keys"
assert direct2 > 0, "Node 2 should hold some keys"
assert direct1 + direct2 == 100, "Every key must be on exactly one node"
def test_hash_client_handles_node_removal():
"""After removing a node, the remaining node serves all keys."""
from testcontainers.memcached import MemcachedContainer
from pymemcache.client.hash import HashClient
with MemcachedContainer("memcached:1.6-alpine") as mc1, \
MemcachedContainer("memcached:1.6-alpine") as mc2:
h1, p1 = mc1.get_host_and_port()
h2, p2 = mc2.get_host_and_port()
full_cluster = HashClient([(h1, p1), (h2, p2)])
for i in range(20):
full_cluster.set(f"k{i}", b"val")
# Simulate node removal: re-create client with one node
degraded = HashClient([(h1, p1)])
# Keys that were on node 2 are now misses - application must
# handle gracefully (cache miss -> read-through from source of truth)
miss_count = sum(
1 for i in range(20) if degraded.get(f"k{i}") is None
)
assert miss_count >= 0 # Some keys lost; app must tolerate itAWS ElastiCache Memcached - Auto Discovery
Per docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/AutoDiscovery.html (opens in new window), ElastiCache Memcached (not Valkey/Redis) supports Auto Discovery: the client connects to a single configuration endpoint and retrieves the full node list. Clients refresh this list approximately once per minute.
def test_elasticache_auto_discovery_endpoint(monkeypatch):
"""
Integration smoke test: verify the app resolves a configuration
endpoint and discovers cluster nodes.
Runs only when ELASTICACHE_CONFIG_ENDPOINT is set.
"""
import os
endpoint = os.getenv("ELASTICACHE_CONFIG_ENDPOINT")
if not endpoint:
pytest.skip("ELASTICACHE_CONFIG_ENDPOINT not set (ElastiCache env only)")
from pymemcache.client.hash import HashClient
# The ElastiCache Cluster Client for Python resolves the cfg endpoint
# and populates the server list automatically via the config get cluster
# Memcached command.
client = HashClient([endpoint])
client.set("smoke-test", b"ok")
assert client.get("smoke-test") == b"ok"The configuration endpoint format is: <cluster-name>.xxxxxx.cfg.<region>.cache.amazonaws.com:11211
Auto Discovery is specific to ElastiCache Memcached and is not available for Valkey or Redis OSS engines (AutoDiscovery docs (opens in new window)).
Running the suite
pytest tests/memcached/ -vTestcontainers boots a Memcached container once per session. The per-test flush_all fixture call resets state between tests. Use scope="session" on the container fixture to avoid the ~3 s startup cost per test.
CI integration
jobs:
memcached-tests:
runs-on: ubuntu-latest
services:
memcached:
image: memcached:1.6-alpine
ports:
- 11211:11211
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v5
- run: pip install -e ".[test]"
- run: pytest tests/memcached/ --tb=short
env:
MEMCACHED_HOST: localhost
MEMCACHED_PORT: 11211For multi-node distribution tests, launch two service containers named memcached-1 and memcached-2 on ports 11211 and 11212.
Memcached protocol-command tests
View source (opens in new window)Memcached protocol-command tests
Deep reference for memcached-tests SKILL.md. Consult when writing the full per-command coverage for a Memcached client - set/get/add, TTL semantics, CAS, and incr/decr. Every test uses the mc fixture defined in the SKILL.
set / get / add
Per docs.memcached.org/protocols/basic/ (opens in new window):
def test_set_and_get(mc):
mc.set("k", b"hello")
assert mc.get("k") == b"hello"
def test_add_only_when_absent(mc):
assert mc.add("k", b"first") is True
assert mc.add("k", b"second") is False # NOT_STORED: key exists
assert mc.get("k") == b"first"
def test_get_absent_returns_none(mc):
assert mc.get("no-such-key") is NoneTTL semantics
Per docs.memcached.org/protocols/basic/ (opens in new window): exptime 0 means never-expire; values up to 30 days are interpreted as a relative second offset; values above 30 days (2592000 seconds) are treated as a Unix timestamp.
import time
def test_ttl_zero_never_expires(mc):
mc.set("k", b"v", expire=0)
time.sleep(0.1)
assert mc.get("k") == b"v"
def test_key_expires_after_ttl(mc):
mc.set("k", b"v", expire=1)
assert mc.get("k") == b"v"
time.sleep(1.5)
assert mc.get("k") is None
def test_short_ttl_via_pexpire_pattern(mc):
# pymemcache does not expose millisecond TTLs; use 1-second minimum
mc.set("k", b"val", expire=1)
time.sleep(1.5)
assert mc.get("k") is None, "Key must expire after 1 s TTL"Avoid time.sleep(60) to test TTL: set the shortest useful TTL and sleep only fractionally beyond it.
CAS (Check-And-Set)
Per docs.memcached.org/protocols/basic/ (opens in new window), gets returns a unique 64-bit CAS identifier; cas stores data only if the token still matches:
def test_cas_succeeds_when_token_matches(mc):
mc.set("k", b"v1")
value, cas_token = mc.gets("k")
result = mc.cas("k", b"v2", cas_token)
assert result is True
assert mc.get("k") == b"v2"
def test_cas_fails_after_concurrent_write(mc):
mc.set("k", b"original")
_, old_token = mc.gets("k")
mc.set("k", b"concurrent-update") # token now stale
result = mc.cas("k", b"late-writer", old_token)
assert result is False # EXISTS: token mismatch
assert mc.get("k") == b"concurrent-update"incr / decr
Per docs.memcached.org/protocols/basic/ (opens in new window), incr/decr operate on unsigned 64-bit integer string values and return None when the key is absent (no auto-initialisation):
def test_incr_increments_existing_counter(mc):
mc.set("counter", b"10")
result = mc.incr("counter", 5)
assert result == 15
def test_incr_absent_key_returns_none(mc):
assert mc.incr("no-such-counter", 1) is None
def test_incr_uses_add_to_initialise(mc):
# Per github.com/memcached/memcached/wiki/Programming:
# add is the correct initialiser for counters
mc.add("hits", b"0")
mc.incr("hits", 1)
assert mc.get("hits") == b"1"
def test_decr_does_not_go_below_zero(mc):
mc.set("counter", b"3")
mc.decr("counter", 10)
assert mc.get("counter") == b"0" # unsigned floor at 0Related skills
browser-cache-control-tests
Wraps browser-side Cache-Control testing with Playwright (Cypress for legacy stacks): asserting response Cache-Control headers from Network events, ETag round-trips (If-None-Match → 304), service-worker strategies (Workbox cacheFirst / networkFirst / staleWhileRevalidate), and reload semantics (normal vs hard). Covers MDN Cache-Control + RFC 9111. Use when auditing browser-tier caching in E2E tests. For CDN-edge purge use cdn-cache-purge-tests; for the reverse-proxy tier use varnish-test-vtc-syntax; for an app-tier store use redis-cache-tests; SWR directive semantics live in stale-while-revalidate-reference.
cache-coherence-patterns-reference
Pure-reference catalog of cache-coherence patterns across the request path. Defines the five-tier cache stack (browser → CDN → reverse-proxy → application → data store), the per-tier cache-writing patterns (cache-aside, write-through, write-back, write-around, refresh-ahead), and the canonical invalidation strategies (TTL-only, event-driven purge, surrogate keys, version-tagged URLs, soft purge), plus an anti-pattern table and a worked multi-tenant coherence-test example. Deep detail - the RFC 9111 Cache-Control / Vary / ETag directive tables and the cross-tier coherence + per-tier test surface - lives in references/. Use for pattern selection, Cache-Control header design, and coherence audits; use a cache-key-collision check when the question is whether two requests in an existing system collide on a concrete key scheme. Consumed by redis-cache-tests, cdn-cache-purge-tests, varnish-test-vtc-syntax, browser-cache-control-tests, and the cache-key-collision check.
cache-key-discriminator-audit
Audits whether a cache key carries every discriminator the cached response actually depends on, so two requests that must not share a slot cannot collide. Ranks identity discriminators (tenant, user, authorization scope, plan tier) above presentation ones (locale, region, currency, feature flag), maps each missing discriminator to the data-exposure or wrong-content consequence it causes, classifies each key-and-value pair into a critical / high / medium severity band (including the Python lru_cache-on-an-instance-method trap), and writes the fix as a namespaced key builder plus the matching HTTP Vary header. Use when a cache key is being designed or changed, when a per-user or per-tenant response is about to be stored in a shared cache or CDN, or when investigating a report that one user or tenant saw another's data.
cache-stampede-reference
Pure-reference catalog of cache-stampede (thundering-herd) phenomena and mitigations: parallel misses on key expiry trigger simultaneous recomputation (often congestion-collapse), countered by three families - locking, external recomputation near-expiry, and probabilistic early expiration via XFetch (`(time() - delta * beta * log(rand(0,1))) >= expiry`). Use when designing cache-refresh strategy or diagnosing a stampede incident. This is the single failure-mode pattern; for the broader multi-tier pattern catalog use cache-coherence-patterns-reference, for the stale-while-revalidate / stale-if-error extensions use stale-while-revalidate-reference; a reference consumed by redis-cache-tests, not a runnable test.
cdn-cache-purge-tests
Wraps CDN cache-purge testing patterns for Cloudflare (POST /zones/{zone_id}/purge_cache, single-file / everything / cache-tags / hostname / prefix), Fastly (POST purge-by-key / purge-all, surrogate-keys via Surrogate-Key header), and CloudFront (CreateInvalidation API + paths). Covers end-to-end test patterns (write origin → trigger purge → assert edge serves fresh), purge-propagation delay testing (typically 1-30s globally), surrogate-key + cache-tag patterns for group-purge, and Cache-Status header verification (cf-cache-status: HIT/MISS/BYPASS). Use when designing or auditing CDN cache-invalidation workflows.
redis-cache-tests
Wraps Redis cache testing: EXPIRE / PEXPIRE / TTL verification (Redis 7+ NX/XX/GT/LT flags), cache-aside write-then-invalidate (write source → DEL key → assert fresh read), eviction under memory pressure (maxmemory + allkeys-lru), pub/sub invalidation across nodes, and tenant key-namespacing. Use when Redis is the app's primary cache. For a Memcached app-tier store use memcached-tests; for the browser/HTTP tier use browser-cache-control-tests; a runnable test, not the cache-stampede-reference or cache-coherence-patterns-reference catalogs.
stale-while-revalidate-reference
Pure-reference catalog of RFC 5861's stale-while-revalidate + stale-if-error Cache-Control extensions. Defines stale-while-revalidate=N (caches MAY serve a stale response while asynchronously revalidating, up to N seconds after expiry) and stale-if-error=N (caches MAY serve stale on 5xx upstream errors). Distinguishes from RFC 9111's must-revalidate (forbids serving stale) and from manual cache-aside refresh (synchronous). Covers the interaction with the freshness lifetime (max-age) and the cache-stampede-mitigation properties. Use when designing the cache-refresh boundary or auditing existing Cache-Control headers.
varnish-test-vtc-syntax
Wraps the varnishtest CLI + VTC (Varnish Test Case) syntax for testing VCL configurations. Covers the VTC test-file format (varnishtest scripts with server { ... } + client { ... } + varnish v1 -vcl+backend { ... } blocks), the grace-mode + saint-mode behaviours (stale-while-revalidate + stale-if-error equivalents in VCL), the PURGE method handler pattern (vcl_purge subroutine + ACL guards), and surrogate-key invalidation via xkey vmod. Use when authoring or testing Varnish-based caching layers.