Why a custom LocMemCache?¶
django-cachex ships its own LocMemCache rather than backing a RespCache
with fakeredis. This page captures
the reasoning so the next maintainer doesn't have to re-litigate the
trade-off from first principles.
What our LocMemCache does¶
django_cachex.cache.LocMemCache extends Django's built-in
LocMemCache with the full cachex extension surface (lists, sets,
hashes, sorted sets, TTL ops, key scanning, admin info), implemented
natively against the underlying in-process dict.
It mirrors RESP semantics where it counts:
- A key's RESP type is determined by the command that wrote it. A plain
Python
liststored viacache.set()is opaque ("string" in RESP terms); only values created vialpush/rpushcarry the_Listtag and are accepted by the list typed accessor. Cross-type access raisesWrongTypeError(aTypeErrorsubclass), the same exception the RESP adapters raise after translating their backend'sWRONGTYPEresponse. - Tagged collections (
_List/_Set/_Hash/_ZSet) live as long-lived Python objects inself._collections, separate from Django's pickled-bytesself._cache. Mutations are in-place under the cache lock; no pickle round-trip per op. - Sorted sets carry a
sortedcontainers.SortedListsidecar, sozrange/zrank/zpopminare O(log N) instead of the O(N log N) re-sort the naive dict-based approach pays.
Why not just use fakeredis?¶
fakeredis is a pure-Python Redis
emulator with full protocol fidelity. In principle we could delete
~1500 lines of LocMemCache code and ship a RedisCache configured
with a FakeRedis client instead. We benchmarked it; the answer is no.
Per-op latency (microseconds, single-threaded, best of 3)¶
| Op | LocMem (any N) | fakeredis-backed (any N) | Real Redis container | AWS ElastiCache |
|---|---|---|---|---|
set/get/del |
2-3 | ~620 | ~100 | ~1000 |
lpush/lpop |
2-3 | ~620 | ~100 | ~1000 |
zrange head 10 |
2.5 | ~680 | ~100 | ~1000 |
zrank |
1.5-2 | ~620 | ~100 | ~1000 |
hgetall (10k fields) |
38 | 14 600 | n/a | n/a |
lrange 0..-1 (10k) |
52 | 21 500 | n/a | n/a |
hset (10k-field hash) |
1.7 | 1 600 | n/a | n/a |
(Reproduce: python -m django_cachex.benchmarks.locmem_vs_fakeredis
isn't checked in; it's a one-off, see the commit that introduced this
page for the script.)
What this tells us¶
- fakeredis has a ~600 µs floor regardless of op or N. That's the cost of the redis-py client serializing the command to RESP wire format, fakeredis interpreting it, formatting the response, and redis-py deserializing back. Even though everything is in-process, the protocol layer dominates.
- fakeredis is slower than real Redis on localhost. At ~620 µs per op it runs ~6x behind a container on the same pod (~100 µs), landing between local Redis and a cross-AZ ElastiCache round-trip.
- Our
LocMemCacheis ~250-400× faster per single op than fakeredis, and the gap widens with N for collection-spanning ops (hgetall,lrange 0..-1) where fakeredis's internal data structures aren't tuned for big working sets.
Where that matters¶
- Test suites. A 12k-test suite that touches the cache ten times
per test would pay an extra ~70 s of wall-clock per run on the
fakeredis path. Multiplied across CI, contributor laptops, and the
inner-loop
pytest -k foocycle, that compounds. - Dev REPL feedback. A cache op that takes ~1 ms instead of ~2 µs changes the feel of "let me poke this in the shell" from instantaneous to noticeable.
TieredCacheL1 hits.LocMemCacheis the default L1 in front of a Redis L2. Inflating the L1 hit cost defeats the point.
Where fakeredis would actually be useful¶
For testing the adapter layer (redis-py / valkey-py command paths,
the WRONGTYPE translation, pipelines, scripts) without standing up a
testcontainer Redis. We currently rely on a real Valkey/Redis container
for adapter integration tests, which is correct but requires Docker.
We haven't wired this in. It would be a [testing] extra and a
fixture, not a runtime dependency, and the testcontainer path is fast
enough today (~140 s for the full suite) that the marginal benefit
hasn't justified the setup. Worth revisiting if a contributor needs to
work without Docker, or if the suite gets meaningfully larger.
Latency context¶
For perspective, where each backend lives on the latency curve:
LocMemCache (in-process): ~2 µs (0.002 ms)
fakeredis + RespCache: ~620 µs (0.6 ms)
Redis on same pod: ~100 µs (0.1 ms)
AWS ElastiCache (cross-AZ): ~1000 µs (1.0 ms)
LocMemCache is the fastest option by two orders of magnitude. It's
also the only option that doesn't pay protocol-serialization cost. Use
it as L1 in TieredCache, as the dev/test backend, and anywhere
in-process caching dominates the access pattern.