Plate 29
functools.cache vs lru_cache(None): Localhost Lab
Aditya Challa4 min read
Intro — what this post promises
Memoize with functools.cache, lru_cache(maxsize=None), and a manual dict. This lab reports warm calls/s on Linux localhost.
It is not lru-cache hit/miss sizing (lab 52). Focus: unbounded cache vs the cache() alias vs hand-rolled dict.
Related links:
- lru cache hit miss localhost lab
- graphlib topo vs manual localhost lab
- tomllib vs json localhost lab
- path glob vs fnmatch localhost lab
- heapq merge vs sorted localhost lab
- islice vs list slice localhost lab
- dataclass replace vs manual localhost lab
- zoneinfo vs utc offset localhost lab
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. Warm paths after fill.
Verdict up front: poly warm (5000 keys) — cache ~23517348 calls/s ≈ lru_cache(None) ~23540822 > manual ~14943394; single-key hits — lru ~28187316, cache ~25639379, manual ~11053470.
Arms
| Arm | Notes |
|---|---|
@cache | unbounded; alias of lru_cache(maxsize=None) |
@lru_cache(maxsize=None) | same unbounded semantics |
manual dict | explicit in / store |
| fib(30) recursive | cold fill + warm |
poly over range(5000) | many keys |
| hit same arg ×200k | lookup micro |
Lab topology
Script: lab-evidence/115-functools-cache-vs-lru/results/run_lab.py.
Lead table — warm poly (5000 args, p50)
| Arm | calls/s |
|---|---|
| lru_cache(None) | 23540822 |
| functools.cache | 23517348 |
| manual dict | 14943394 |
Hit micro & fib warm
| Arm | calls/s |
|---|---|
| hit lru_cache(None) | 28187316 |
| hit functools.cache | 25639379 |
| hit manual dict | 11053470 |
| fib cache warm | 8695916 |
| fib lru warm | 7407511 |
| fib manual warm | 6134846 |
cache and lru_cache(None) track each other — expected, since cache is documented as that unbounded wrapper.
Alias clarity
In CPython, functools.cache is implemented as lru_cache(maxsize=None). Treat them as one mechanism with two spellings — pick cache for readability unless you already standardize on lru_cache across the codebase.
Cold vs warm
Cold fib fills ~N entries quickly; warm calls are essentially a single hash lookup returning the root. Poly cold must insert 5000 keys once — after that, wrappers stay ahead of a pure-Python if x in d loop on this box.
Reading it
- Prefer
@cachefor unbounded pure memo (clearer thanmaxsize=None). - Need eviction → sized
lru_cache(lab 52 for hit/miss sizing). - Manual dict only when you need custom keys/eviction/metrics.
- Typed/unhashable args still fail — wrappers need hashables.
When manual dict still wins
Custom composite keys, soft references, metrics counters, or per-tenant partitions often force an explicit dict (or WeakValueDictionary — see the weakref cache lab). For plain hashable args and unbounded memo, prefer @cache and skip maintaining your own if k in d boilerplate.
Threading caveat
lru_cache / cache are generally fine for concurrent reads of already-populated entries, but fill storms and cache_clear need care in multi-threaded servers. This lab is single-threaded warm lookups only — do not extrapolate to lock-free claims.
Bounded sibling
If RSS growth is the fear, switch to lru_cache(maxsize=…) and tune with lab 52’s hit/miss methodology. Unbounded wrappers here intentionally match “remember everything” workloads, not the eviction design space.
Pitfalls
- Unbounded growth on huge key spaces (memory leak).
- Caching impure functions (time, I/O, globals).
- Comparing to lab 52’s bounded hit/miss story without noting maxsize.
- Clearing caches in tests but not in long-running workers.
Reproduce
Evidence: summary.json, summary.txt.
Limits
One Linux box. Hashable int keys only. Not threaded cache contention. Not typed=True variants.
Takeaway
Warm poly lookups: cache ≈ lru_cache(None) ~23517348 calls/s beat manual ~14943394. Use @cache for unbounded memo; use sized lru_cache when you need eviction (see lab 52).
Lab evidence
What I found running this
Ran the supplied run_lab.py on Linux localhost with Python 3.13.5, warming cache paths after fill for 7 p50 rounds. The poly workload used 5000 keys; repeated-hit microbench used 200k inner hits; fib used N=30. Warm poly measured lru_cache(None) at 23,540,822 calls/s, functools.cache at 23,517,348, and manual dict at 14,943,394. Single-key hits favored lru_cache, while cache and lru_cache tracked closely as expected aliases.
Related links
Plate 34
ast.literal_eval vs json.loads: Localhost Lab
1 Oct 2026
Plate 68
gc.collect Cost Empty vs Cycles: Localhost Lab
Hands-on gc.collect cost empty vs cyclic garbage lab: real collect latency plus reclaim counts, measured on Linux localhost today in this lab for SREs.
Observability & SRE · 1 Oct 2026
Plate 76
html.parser vs regex Tag Strip: Localhost Lab
1 Oct 2026