4 min read
There is no real difference between functools.cache and lru_cache(maxsize=None): in CPython, cache is implemented as lru_cache(maxsize=None), and on Python 3.13.5 both ran about 23.5M warm calls/s over 5,000 keys, against about 14.9M for a hand-written dict. Use @cache for unbounded memoization because it reads more clearly, and switch to a sized lru_cache(maxsize=...) when memory growth means you need eviction.FAQ
It is not lru-cache hit/miss sizing (lab 52). Focus: unbounded cache vs the cache() alias vs hand-rolled dict.
Related links:
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.
| 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 |
Script: lab-evidence/115-functools-cache-vs-lru/results/run_lab.py.
| Arm | calls/s |
|---|---|
| lru_cache(None) | 23540822 |
| functools.cache | 23517348 |
| manual dict | 14943394 |
| 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.
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 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.
@cache for unbounded pure memo (clearer than maxsize=None).lru_cache (lab 52 for hit/miss sizing).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.
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.
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.
Evidence: summary.json, summary.txt.
One Linux box. Hashable int keys only. Not threaded cache contention. Not typed=True variants.
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).
Yes. functools.cache is lru_cache(maxsize=None), an unbounded cache with no eviction. In this lab they tracked each other: 23,517,348 vs 23,540,822 warm calls/s.
Here, yes. Warm lookups over 5,000 keys ran at about 23.5M calls/s with the decorators vs about 14.9M with an if-key-in-dict loop. Single-key hits were 28.2M (lru_cache) vs 11.1M (dict).
When the key space is large or comes from user input. An unbounded cache keeps every key and grows memory, so set maxsize to cap it.
Lab evidence
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.
Explore the product
Untested by us. Check what's included, the license and refund terms before you buy.
Summary1 Oct 20263 min read
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.
Hands-on1 Oct 20264 min read
Hands-on1 Oct 20264 min read