1.047 Caching Libraries#

Python caching, measured: lru_cache is a dict lookup, cachetools 20× that, diskcache 350 µs a miss, redis-py 106 µs a trip — and LRU scores 0% on a loop.

Quick guide

One process? functools.lru_cache — 0.03 µs a hit, a dict lookup.

Need a time-to-live, a size in bytes, or a non-LRU policy? cachetools — 0.5–1.2 µs, and that is nothing next to anything worth caching.

One machine, across restarts? diskcache — 4 µs a hit, 350 µs a miss (18 µs batched in one transaction).

Many machines? redis-py against Valkey or Redis — 106 µs a trip, 7 µs a key pipelined; same number either server.

Before any of it, measure the access pattern. LRU scores 0% on a loop larger than the cache; LFU loses 16 points when the favourites move; with no favourites the hit rate is the cache size.

Try it: cache me if you can — a working demo of this survey’s findings. In your browser: what a hit and a miss cost on each tier, which eviction policy earns the hit rate on the access pattern you pick — replayed through cachetools’ own classes against the offline optimum — and the hit rate under which the cache makes your code slower, for a function of the cost you set.

At a glance#

Findings checked against this survey’s current text on 2026-08-25.

diskcache one store
19×
in one transaction 18 µs · single set 350 µs
redis-py GET per key
15×
100 pipelined 7 µs · one at a time 106 µs
one cache hit per tier lower is faster
functools.lru cache: 0.03 µsfunctools.lru cache0.03 µscachetools LRUCache: 0.55 µscachetools LRUCache0.55 µscachetools TTLCache: 1.2 µscachetools TTLCache1.2 µsdiskcache.get: 4 µsdiskcache.get4 µsredis-py → Valkey (socket): 106 µsredis-py → Valkey (socket)106 µs
LibraryHow it worksBest forLatest release
functools.lru_cacheStandard library, C; LRU only, count-bounded, no expiryThe first reach in one process — any function worth caching, until it needs a clock or a weight
cachetoolsPure Python; LRU, LFU, FIFO, RR, TTL, TLRU cache classes and a cached() decoratorIn-process caching with a time-to-live, a weight function, inspection, or a policy other than LRU7.1.8 · 2026-08-31
diskcachePure Python over SQLite; per-process and per-thread safe; memoize(), Cache, FanoutCacheOne machine, values that must survive a restart, no server5.6.3 · 2023-08-31
redis-pyRedis Inc.’s client, MIT; RESP3 by default since 8.0; sync and asyncioThe distributed tier — and the data structures, TTLs and atomic counters that make it more than a cache8.1.0 · 2026-07-30
Valkey (server; clients redis-py, valkey-glide, valkey-py)Linux Foundation fork of Redis 7.2.4, BSD-3-Clause; 9.1.1 (2026-07-21)The distributed tier when the license matters, or on AWS/GCP where it is the cheaper managed engine6.1.1 · 2026-03-18
pymemcachePinterest’s pure-Python memcached client, Apache-2.0; set() is noreply by defaultA shared cache and nothing else, when you want the server to be incapable of becoming more4.0.0 · 2022-10-17
dogpile.cachePure Python front end over memory, file, Redis and memcached backends, MIT; Mike BayerOne API over two backends, and the stampede lock — one miss recomputes, the rest wait1.5.0 · 2025-10-11

Latest release observed from PyPI in 2026-09.

What the research found

  • The standard library’s lru_cache is a dict lookup; cachetools is twenty times that, and neither number matters — 0.03 µs against 0.55 µs (LRU) and 1.2 µs (TTL). What cachetools buys for the microsecond is a time-to-live, a size in bytes, inspection and five policies. A database hit costs a thousand of those microseconds and an HTTP call ten thousand. The one case the overhead decides is a function under ~10 µs — and the answer there is not to cache it.
  • A cache is paid for by its misses, and on diskcache a miss costs a hundred hits — get 4 µs; a single set 350 µs on a real disk. For a 100 µs function the cache breaks even at an 82% hit rate; for 10 ms, at 4%. Batched inside one transact() the write is 18 µs. The writes, not the reads, are what a disk cache charges for.
  • The hit rate belongs to the access pattern, not the library — and LRU scores exactly zero on a loop — 30,000 requests over 10,000 keys through cachetools’ own classes. Stable hot set, 10% cached: LRU 67%, LFU 68%, ceiling 79%. Hot set that moves: LRU 66%, LFU 49% — LFU keeps counting the old favourites. No favourites: every policy 10%, the cache size. A loop over everything: LRU and FIFO 0.0% at every size up to half, each key evicted one step before it comes round. Measure the pattern before choosing the policy; LRU is the default for the right reason — it recovers.
  • On the network tier the round trip is the price, and the server is not the difference — One synchronous Python client: 106 µs median per GET over a unix socket (45 µs at best), 320 µs over this machine’s TCP loopback, and the raw bare-socket floor is 11 / 112 µs — so the client costs ~35 µs and the OS the rest. Valkey 9.1.1 and Redis 8.10.1 are the same number behind redis-py (106 vs 110). pymemcache is 30 µs closer to the floor at the minimum and equal at the median. A pipeline makes every client 3–10 µs per key — the only order-of-magnitude lever on this tier.
  • Since 2024 the distributed tier has two servers and one client — Redis 7.4 left BSD for RSALv2/SSPLv1 in March 2024; Valkey forked 7.2.4 under the Linux Foundation the same month (BSD-3-Clause); AWS priced ElastiCache for Valkey 20–33% below Redis OSS in October 2024; Redis 8 added AGPLv3 in May 2025. redis-py speaks to both unchanged. The door is one-way: Valkey reads Redis ≤ 7.2 data files, not 7.4+ — decide before that upgrade.

Explainer

Domain Explainer: Caching Libraries#

For readers deciding whether this category is relevant to them, and what they need to know before reading the passes.


The whole answer, first#

A cache keeps the answer to a question so the next time it is asked, nobody has to work it out again. Finding the answer already there is a hit; not finding it — and having to work it out and store it — is a miss. In Python there are three places to keep a cache, and the place decides the library. Measured for the survey on one machine, a hit on the cheapest possible function:

Who needs the answerKeep itA hit costsA miss adds
the same processfunctools.lru_cache — standard library0.03 µs, a dict lookup0.07 µs
the same process, and it must expire or be bounded by sizecachetools0.5–1.2 µs0.4–1.4 µs
the same machine, across restartsdiskcache — a database in a file, no server4 µs350 µs — a disk write; 18 µs batched
other machinesredis-py → Valkey or Redis106 µs — a round tripanother trip

The fourth number is the one the category hides: what hit rate will your traffic give the cache? The hit rate is the share of requests the cache answers, and it is a property of the traffic, not the library. A cache is always smaller than the set of things it could hold, so when it is full it must throw something out, and the rule for choosing is its eviction policy: LRU (least recently used) discards whatever was asked for longest ago; LFU (least frequently used) whatever was asked for fewest times; FIFO (first in, first out) whatever arrived earliest; random, at random. On a stable hot set — a few keys getting most requests — with a cache holding 10% of the keys, LRU gets 67%. When the favourites move, LRU keeps 66% and LFU drops to 49%. On a loop over more keys than fit, LRU gets 0% — at any size up to half.

That is the finding. The rest of this page is why, and all of it runs in your browser at /workshop/cache-me/.


The one-paragraph version#

Caching looks like one decision and is three. The first is the tier: who reads the cache — one process, one machine, or many — and that is set by your architecture before any library is chosen. The second is the miss: every tier makes hits cheap, and charges for misses — the function and the store, which on a disk is a write and on a network is a second trip — so a cache on a fast function with a middling hit rate makes the code slower, and the break-even is a number you can compute. The third is the policy, and it matters less than people think on ordinary traffic and more than they think on two kinds: traffic whose favourites move (LRU recovers, LFU does not) and traffic that loops over more than fits (nothing recovers). The libraries are all good; the three decisions are made before any of them is chosen.


What a cache does, and what it costs#

The in-process tier. functools.lru_cache is a C dictionary keyed on the function’s arguments, evicting the least recently used entry past a count. A hit is a hash lookup and a pointer move: 0.03 µs. It cannot expire an entry on a clock and cannot bound itself by bytes. cachetools can — TTLCache, a getsizeof weight, LRUCache / LFUCache / FIFOCache / RRCache, caches you can inspect and clear — and it is pure Python, so a hit is a decorator, a key, a method call and, for TTL, a clock read: 0.55–1.2 µs. Twenty to forty times the standard library, and invisible next to any function worth caching; a local database query is a thousand of those microseconds and an HTTP call ten thousand. The one case the overhead decides is a function that costs less than about 10 µs, and the answer there is not to cache it.

The on-machine tier. diskcache keeps the cache in SQLite — a database that lives in an ordinary file — so it survives a restart, is shared by every process on the box, and needs no daemon. A read is 4 µs. A write is a SQLite transaction: 350 µs on a real disk, and 18 µs per key when two hundred of them share one transact(). That asymmetry sets its break-even: a 100 µs function needs an 82% hit rate for diskcache to pay; a 10 ms one needs 4%. It is the tier for results that are expensive — queries, downloads, model artifacts — on one machine.

The network tier. redis-py talks to a Valkey or Redis server; pymemcache to memcached. Every hit and every miss is a round trip. Measured with the network taken out — over a unix socket, a connection between two processes on the same machine that skips the network stack — one synchronous Python call is 106 µs median with redis-py and 71 µs with pymemcache; the raw floor under both is 11 µs, so the clients cost 35 and 3 µs respectively and the medians are the operating system’s. Over this machine’s TCP loopback the same call is 320 µs; a real network adds its own latency on top. A pipeline — one trip for a hundred keys — makes every client 3–10 µs per key, and is the single order-of-magnitude lever on this tier. The servers are not the difference: Valkey 9.1.1 and Redis 8.10.1 measure the same behind the same client.


The policy question#

The survey replayed 30,000 requests over 10,000 keys through cachetools’ own cache classes, one per policy, with a cache holding 10% of the keys, and compared them with the ceiling — the best any policy could do if it could see the future:

TrafficLRULFUFIFOrandomceiling
a stable hot set67%68%63%64%79%
a hot set that moves66%49%62%61%76%
a hot set plus a scan49%53%45%45%64%
no favourites10%10%10%10%38%
a loop over everything0%7%0%0%7%

On a stable hot set the choice barely matters, and a cache holding one key in a hundred already catches two requests in five — the first few percent of cache do most of the work. When the hot set moves, LRU forgets in one cache-length and LFU keeps counting the old favourites: 16 points. A scan through everything pollutes LRU (it dutifully caches each scanned key once) and not LFU; newer policies that admit a key only once it has proved itself — W-TinyLFU, ARC — exist for that case, and cachetools does not ship one. With no favourites the hit rate is the cache size and no policy changes it. And a loop larger than the cache defeats LRU and FIFO completely: each key is evicted one step before it comes round again, every pass, at every size up to half. The only remedies are a cache at least as large as the loop, or no cache.

One more thing, two lines long: the textbook sequence on which a bigger FIFO cache misses more — Belady’s anomaly — reproduces in cachetools’ FIFOCache (9 misses with 3 slots, 10 with 4). LRU cannot do that. It is the reason the default is LRU and not FIFO.


The server question, since 2024#

Behind redis-py there are now two servers. Redis 7.4 left the BSD license for RSALv2 / SSPLv1 in March 2024; the Linux Foundation forked Redis 7.2.4 as Valkey (BSD-3-Clause) the same month; AWS priced ElastiCache for Valkey 20–33% below Redis OSS that October; Redis 8 added AGPLv3 as a third option in May 2025. The wire protocol is the same, the client is the same, the measured round trip is the same. What differs is the license your lawyer will accept, the managed SKU your cloud offers, and one door that only opens one way: Valkey reads data files written by Redis 7.2 and earlier, not 7.4 or later. Decide before that upgrade, not after.


What to do#

  1. Name the tier. Who reads the cache — one process, one machine, many? That is the library.
  2. Cost the miss. What does the function cost, what does the store cost on that tier, and what hit rate does that imply for break-even? The floor model computes it for the cost you set.
  3. Measure the pattern. Replay a day of keys through the policies before choosing one — or accept LRU, which is the default for a good reason.
  4. Never fetch two keys in two trips on the network tier.
  5. Batch writes on diskcache.

And if the traffic is a loop over more than fits, the right amount of cache is either all of it or none.

S1: Rapid Discovery

S1 - Rapid Discovery: Caching Libraries#

Every figure measured 2026-08-20 for the cache-me floor model (/workshop/cache-me/). Download counts are pypistats.org, last 30 days, read 2026-08-20. Timings are what a Python caller sees on one machine (CPython 3.14.7, aarch64, single thread); the method and the full tables are in S2 §0, and the bench imports the page’s own core.py.

The pick#

Three tiers, one library each — and a question to answer before any of them.

Who reads the cacheUseMeasured
one processfunctools.lru_cache (stdlib)0.03 µs a hit — a dict lookup
one process, and it needs a clock, a weight or a policycachetoolsLRU 0.55 µs, LFU 0.8 µs, TTL 1.2 µs a hit
one machine, across restartsdiskcache4 µs a hit; 350 µs a miss (a disk write), 18 µs batched
many machinesredis-py → Valkey or Redis106 µs a trip (45 µs at best); 7 µs a key pipelined
many machines, and the server should do nothing elsepymemcache → memcached71 µs a trip (14 µs at best)
two of the above behind one API, or a hot key that stampedesdogpile.cachebackend + 5 µs
from functools import lru_cache
@lru_cache(maxsize=4096)                       # 0.03 µs a hit; start here
def price(sku): ...

import cachetools
@cachetools.cached(cachetools.TTLCache(4096, ttl=300))   # 1.2 µs a hit, and it expires
def rate(currency): ...

import diskcache
cache = diskcache.Cache("/var/cache/app")
@cache.memoize(expire=3600)                    # 12 µs a hit; survives a restart
def report(day): ...
with cache.transact():                         # 18 µs a write instead of 350
    for k, v in bulk: cache.set(k, v)

import redis
r = redis.Redis(host="cache")                  # Valkey or Redis — same client, same number
r.setex(f"user:{uid}", 3600, blob)             # 106 µs a trip
with r.pipeline(transaction=False) as p:       # 7 µs a key
    for k in keys: p.get(k)
    values = p.execute()

The question before the library: what hit rate will your traffic give it?#

30,000 requests over 10,000 keys, a cache holding 10% of them, replayed through cachetools’ own classes. The numbers are deterministic and the floor model re-runs them on any pattern and size.

Your traffic looks likeLRULFUFIFOrandomthe ceiling
a stable hot set67%68%63%64%79%
a hot set that moves66%49%62%61%76%
a hot set plus a scan49%53%45%45%64%
no favourites10%10%10%10%38%
a loop over everything0%7%0%0%7%

Three things to read off it. On a stable hot set the policies are within a few points and a cache holding 1% of the keys already catches 39% of requests. When the favourites move, LRU follows and LFU keeps counting yesterday’s — 16 points. And on a loop that does not fit, LRU and FIFO score exactly zero at every size up to half: each key is evicted one step before it comes round again. No better LRU fixes that; only a cache at least as big as the loop, or none.

What a hit costs, what a miss costs#

hitstore (what a miss adds)
dict0.02 µs0.02 µs
functools.lru_cache0.03 µs0.07 µs
cachetools LRUCache0.55 µs0.37 µs
cachetools LFUCache0.8 µs0.3 µs
cachetools TTLCache1.2 µs1.4 µs
cachebox (Rust, not in the core)0.28 µs
dogpile.cache, memory backend5.4 µs
diskcache.get / .set4 µs350 µs on disk; 18 µs in one transaction
diskcache.memoize12 µssame
redis-py → Valkey or Redis, unix socket106 µs median (45 min)113 µs
redis-py, this machine’s TCP loopback320 µs (177 min)365 µs
pymemcache → memcached, unix socket71 µs median (14 min)68 µs
any of them, 100 keys pipelined3–10 µs per key

A cache pays when the hit rate clears store ÷ (function + store − hit). With diskcache’s 350 µs write, a 100 µs function needs 82% hits to break even and a 10 ms one needs 4%; over a socket, a 100 µs function needs 52%. The floor model’s third panel does this arithmetic for the function cost you set.

Downloads a month: cachetools 339M, redis 275M, diskcache 37M, valkey 5.1M, pymemcache 3.3M, dogpile.cache 2.5M, python-memcached 1.3M, valkey-glide 1.0M.

Three things that decide most choices#

The tier decides before the library. Inside one process the standard library is the fastest cache in Python and the one to start with; cachetools is the same tier with a clock and a weight. diskcache is a different tier — persistence without a server — and the network is a third. Nothing in a lower tier can be made to serve a higher one, and nothing in a higher one is as cheap.

The miss is the price. Hits are cheap everywhere. What a tier charges is the store after a miss and, on the network, the trip either way. Put a number on the function before adding the cache; if it costs under ten microseconds, the decorator is the cost.

Redis and Valkey are the same number. Behind redis-py, 106 µs and 110 µs. The choice between them is license (Redis 8: RSALv2 / SSPLv1 / AGPLv3; Valkey: BSD-3-Clause) and the cloud bill (ElastiCache for Valkey is 20–33% cheaper), and it is one-way past Redis 7.2 because the data-file format diverged. S2 §0 has the dates, the versions and the three clients.

When each one stops being a reason#

You choseIt stops paying when
lru_cacheentries must expire on a clock, or memory must be bounded by bytes
cachetoolsthe wrapped function costs under ~10 µs
cachetools LFUthe favourites move — LRU recovers, LFU remembers
diskcachethe function is under ~1 ms and the hit rate under ~30%; or you bulk-load without transact()
redis-py / pymemcacheevery reader is one process; or you fetch N keys in N trips
valkey-glideyou may go back to Redis 8 (outside its engine table), or the load is one call at a time
dogpile.cachenothing needs the lock or the backend swap
any cachethe traffic is a loop larger than the cache

Install#

# functools.lru_cache — standard library
pip install cachetools          # pure Python, MIT, 7.1.7
pip install diskcache           # pure Python over SQLite, Apache-2.0, 5.6.3
pip install redis               # redis-py, MIT, 8.1.0 — speaks to Valkey and Redis
pip install pymemcache          # Apache-2.0, 4.0.0
pip install dogpile.cache       # MIT, 1.5.0

Measured: 2026-08-20

S2: Comprehensive

S2 Comprehensive Discovery: Caching Libraries#

Measured: 2026-08-20 (§0); library notes and patterns below Methodology: S2 - Systematic technical evaluation across performance, features, and ecosystem

0. Measured 2026-08-20 — what building the floor model established#

Everything in this section was measured, not read: one machine — CPython 3.14.7, aarch64 (Qualcomm Snapdragon X under WSL2), single thread — with the bench importing the floor model’s own core.py (01-discovery/bench/, results in results.json), and the in-process and on-disk tiers run again in the browser at /workshop/cache-me/ (Pyodide 314.0.5, Python 3.14.2) at about a third of native speed, same shape, identical hit rates. Versions: cachetools 7.1.7, diskcache 5.6.3, redis-py 8.1.0, valkey-py 6.1.1, valkey-glide 2.5.1, pymemcache 4.0.0, dogpile.cache 1.5.0, cachebox 6.2.5; servers Valkey 9.1.1, Redis 8.10.1 and memcached 1.6.38 built from source on the same machine. The server and client license facts were read from the projects' own pages the same day (§0.5).

0.1 What a hit costs, what a store costs#

Each cache wraps the cheapest possible function, k + 1, so the figure is the cache’s own overhead. 1,000 warmed keys, 1,000 calls per batch, best-of under a budget.

hitstore
the function itself0.023 µs
dict0.019 µs0.022 µs
functools.lru_cache (stdlib, C)0.030 µs0.075 µs
cachetools LRUCache0.55 µs0.37 µs
cachetools LFUCache0.79 µs0.29 µs
cachetools TTLCache1.17 µs1.43 µs
cachebox LRUCache (Rust; native only)0.28 µs
dogpile.cache, memory backend (native only)5.4 µs
diskcache get3.9 µs
diskcache memoize()11.6 µs
diskcache set, one key351 µs (184–444 across runs)
diskcache set, 200 keys in one transact()18.5 µs per key

In the browser (Pyodide, MEMFS): lru_cache 0.15 µs, cachetools LRU 1.85 µs, TTL 3.4 µs, diskcache get 6.6 µs — same order, roughly 3× slower. diskcache set is 55 µs there because nothing is fsynced; the page says so and shows the disk figure in orange.

Memory per entry (tracemalloc, 10,000 int → int): dict 61 B, lru_cache 116 B, LRUCache 135 B, LFUCache 142 B, TTLCache 223 B. The TTL link costs a dict’s worth again.

0.2 Hit rate by policy and pattern#

30,000 requests over 10,000 keys, seed 7, replayed through cachetools’ own classes with __getitem__ (so a hit updates recency), against functools.lru_cache (identical to LRUCache on every row) and Belady’s offline MIN as the ceiling. “Hot set” is Zipf, s = 1, over a shuffled key space; “moves” re-shuffles the favourites every third of the run; “plus a scan” mixes one sequential request in four.

patterncacheLRULFUFIFOrandomceiling
stable hot set1%38.9%44.9%34.2%34.4%58.0%
5%58.9%61.2%54.3%54.1%73.6%
10%67.3%68.4%63.1%63.5%78.7%
25%77.3%77.3%74.5%75.1%82.1%
50%82.1%82.1%81.5%82.0%82.1%
hot set that moves1%38.7%30.1%34.1%34.3%57.5%
10%65.7%49.4%62.1%61.2%76.2%
25%74.1%64.0%73.2%71.5%79.1%
hot set plus a scan1%26.5%32.1%22.8%22.7%44.6%
10%48.6%52.6%44.7%44.9%64.4%
25%58.8%63.5%55.8%56.3%70.8%
no favourites10%9.8%9.9%9.8%9.6%37.6%
50%44.9%45.0%44.6%45.0%64.7%
loop over everything1%0.0%0.6%0.0%0.0%0.7%
10%0.0%6.7%0.0%0.0%6.7%
25%0.0%16.7%0.0%1.4%16.7%
50%0.0%33.3%0.0%14.3%33.3%

Belady’s anomaly, the sequence 1 2 3 4 1 2 5 1 2 3 4 5 through cachetools: FIFOCache 9 misses with 3 slots, 10 with 4; LRUCache 10 → 8.

0.3 The network tier, per operation#

Per-operation min / median / p90 over 3,000 calls, one synchronous Python client. “unix” is the client’s own cost with the network removed; “tcp” adds this machine’s loopback stack, which under WSL2 is a 112 µs floor by itself. The raw rows are a PING on a bare socket — what the machine charges before any client code runs.

client → serverGET minGET p50GET p90SET p50GET, 100 pipelined, per key
raw socket floor → Valkey (unix)11.5 µs69.5 µs
raw socket floor → Valkey (tcp)111.9 µs234.3 µs
redis-py 8.1.0 → Valkey 9.1.1 (unix)45.2106.3206113.37.1
redis-py 8.1.0 → Redis 8.10.1 (unix)42.5109.5193119.47.0
valkey-py 6.1.1 → Valkey 9.1.1 (unix)35.780.017288.76.1
pymemcache 4.0.0 → memcached 1.6.38 (unix)14.371.113867.92.9
redis-py → Valkey (tcp)176.8319.5532365.410.3
redis-py → Redis (tcp)176.2320.3522326.19.6
valkey-py → Valkey (tcp)145.7315.2508288.98.9
valkey-glide 2.5.1 (asyncio) → Valkey (tcp)249.1550.1841486.427.1 (100 concurrent awaits)
pymemcache → memcached (tcp)137.3271.0457272.35.1

What the table says: the server is not the difference (Valkey and Redis within 3 µs behind the same client); the client is a difference of tens of microseconds at the minimum (pymemcache 3 µs over the floor, valkey-py 24, redis-py 34 — redis-py without the hiredis parser) and none at the median; the operating system is most of the median; and a pipeline is the one lever that changes the tier’s cost by an order of magnitude. valkey-glide’s single-call figure includes an event-loop turn per await — its case is concurrency, and with 100 awaits in flight it is 27 µs per key. pymemcache’s set() is noreply=True by default and returns in 2 µs without a round trip; the SET above asked for the reply.

0.4 Break-even, from the measured costs#

core.payoff(): mean time with cache = h·hit + (1 − h)·(fn + store); the cache pays when h > store ÷ (fn + store − hit).

the wrapped function costscachetools TTL pays at h ≥diskcache (disk write)redis-py, socket
10 µs14%nevernever
100 µs1.4%82%52%
1 ms0.1%26%10%
10 ms~03.4%1.1%

0.5 The servers and clients behind the network tier (read, not measured)#

Read on 2026-08-20 from GitHub release lists via the API, redis.io/legal/licenses, valkey.io/topics/migration, the Valkey 9.0 and 9.1 release posts, the AWS “what’s new” notice for ElastiCache for Valkey and the Memorystore for Valkey product page.

RedisValkey
StewardRedis Inc.Linux Foundation (launch backers AWS, Google, Oracle, Ericsson, Alibaba, Huawei, Tencent)
Forked fromRedis 7.2.4, 2024-03-22
License≤ 7.2 BSD-3-Clause · 7.4 RSALv2 or SSPLv1 · 8+ RSALv2 or SSPLv1 or AGPLv3 (AGPLv3 added 2025-05-01)BSD-3-Clause
Current release8.10.1 (2026-08-17; 7.4.11 and 6.2.24 lines still patched)9.1.1 (2026-07-21; 9.0.5, 8.1.9, 8.0.10, 7.2.14 lines still patched)
GitHub76.1k stars26.9k stars, 1.3k forks
Wire protocolRESP2 / RESP3RESP2 / RESP3; INFO reports redis_version:7.2.4 plus server_name / valkey_version
Data filesRDB / AOFReads Redis ≤ 7.2 RDB / AOF. Redis 7.4+ RDB does not load
ModulesStack bundled in 8.0: JSON, search, time series, Bloom, vector sets — tri-licensedSeparate BSD modules: valkey-json 1.0.2, valkey-bloom 1.0.1, valkey-search 1.1.1, valkey-ldap
ManagedRedis Cloud; ElastiCache “Redis OSS” (older engines)ElastiCache / MemoryDB for Valkey (2024-10-08; 20% cheaper node-based, 33% cheaper serverless, $6/month entry); Memorystore for Valkey (7.2, 8.0, 9.0; 9.1 preview); Aiven; DigitalOcean

Valkey 9.0 (2025-10-21): numbered databases in cluster mode, atomic slot migration, per-field hash expiry (HEXPIRE, HTTL, HPERSIST), DELIFEQ, polygon geosearch, and the project’s headline — over one billion requests per second across 2,000 nodes. 9.1 (2026-05-19): 2.1 million req/s on a single server (512-byte payloads, 9 I/O threads, pipeline depth 10), up to 20% less memory for strings under 128 bytes, ACLs scoped to a database. Redis 8 answered with vector sets and the bundled Stack modules. On the cache path — GET, SET, SETEX, EXPIRE, pipelines, pub/sub — they are the same server with different licenses, and §0.3 measures them as such.

ClientLicenseStarsLatest stableEngines listedMaintainer
redis-pyMIT13.6k8.1.0, 2026-07-30Redis; Valkey by protocol compatibilityRedis Inc.
valkey-glide (Python)Apache-2.0778 (monorepo)2.5.1, 2026-08-10Valkey 7.2–9.0 · Redis 6.2–7.2valkey-io; AWS and GCP named as backers
valkey-pyMIT2806.1.1, 2025-08-11 (6.2.0rc2 2026-03-18)Valkey, Redisvalkey-io, three listed maintainers

Comprehensive Library Analysis#

1. redis-py (Redis Python Client)#

Technical Specifications:

  • Performance: one round trip per call — 106 µs median over a unix socket, 320 µs over TCP loopback on the bench machine (§0.3); 7 µs per key pipelined
  • Memory: server-side; RESP3 on the wire by default since 8.0
  • Features: Pub/sub, transactions, clustering, persistence
  • Ecosystem: Extensive tooling, monitoring, cloud services

Strengths:

  • Industry-proven scalability (Instagram, GitHub, Twitter)
  • Rich data structures (strings, hashes, lists, sets, sorted sets)
  • Built-in persistence and high availability
  • Extensive monitoring and operational tools
  • Active development and enterprise support

Weaknesses:

  • Higher memory overhead than pure cache solutions
  • Network latency for distributed setups
  • Complexity for simple use cases
  • Additional infrastructure dependency

Best Use Cases:

  • Multi-server applications requiring shared state
  • Real-time features (leaderboards, counters, sessions)
  • Complex data structures beyond key-value pairs
  • Applications requiring persistence and high availability

2. python-memcached / pymemcache#

Technical Specifications:

  • Performance: one round trip per call — 71 µs median over a unix socket, 14 µs at best (§0.3); 2.9 µs per key with get_many
  • Memory: server-side, slab-allocated; the client is pure Python
  • Features: Simple key-value storage, LRU eviction
  • Ecosystem: Mature, lightweight, focused

Strengths:

  • Fastest pure caching performance
  • Minimal memory overhead
  • Battle-tested stability (Facebook, Wikipedia)
  • Simple operational model
  • Predictable behavior under load

Weaknesses:

  • No persistence (data lost on restart)
  • Limited data structures (key-value only)
  • No built-in clustering or replication
  • Limited observability features

Best Use Cases:

  • High-frequency API response caching
  • Session storage for stateless applications
  • Database query result caching
  • Maximum performance requirements

3. diskcache#

Technical Specifications:

  • Performance: get 4 µs, memoize 12 µs per hit; set 350 µs on disk, 18 µs per key inside one transact() (§0.1)
  • Memory: minimal in-process; SQLite-backed persistence
  • Features: TTL, LRU, size limits, thread-safe operations
  • Ecosystem: Zero dependencies, pure Python

Strengths:

  • Persistent across application restarts
  • No external infrastructure required
  • Thread-safe and process-safe operations
  • Built-in eviction policies
  • Excellent for development and single-server deployments

Weaknesses:

  • Slower than memory-based solutions
  • Not suitable for distributed applications
  • Filesystem I/O limitations
  • Limited concurrent access performance

Best Use Cases:

  • Single-server applications
  • Development environments
  • Caching large objects or files
  • Applications requiring cache persistence

4. cachetools#

Technical Specifications:

  • Performance: LRU 0.55 µs, LFU 0.8 µs, TTL 1.2 µs per hit (§0.1) — against 0.03 µs for functools.lru_cache
  • Memory: 135 B per entry (LRU), 223 B (TTL), against 61 B for a dict and 116 B for lru_cache
  • Features: LRU, TTL, decorators, multiple eviction strategies
  • Ecosystem: Stdlib-style API, decorator patterns

Strengths:

  • Zero external dependencies
  • Decorator-based usage patterns
  • Multiple cache strategies (LRU, TTL, LFU)
  • Perfect for function memoization
  • Immediate implementation

Weaknesses:

  • Single-process only
  • Memory limited by Python process
  • No persistence across restarts
  • Limited observability

Best Use Cases:

  • Function result caching
  • Single-process applications
  • Prototype development
  • Simple in-memory caching needs

5. dogpile.cache#

Technical Specifications:

  • Performance: Backend-dependent, abstraction overhead
  • Memory: Backend-dependent
  • Features: Multi-backend, regions, key generation, decorators
  • Ecosystem: SQLAlchemy integration, enterprise features

Strengths:

  • Backend abstraction (Redis, Memcached, files, database)
  • Advanced features (regions, key namespacing, decorators)
  • SQLAlchemy integration for ORM caching
  • Enterprise-grade locking and dogpile prevention
  • Flexible configuration management

Weaknesses:

  • Additional abstraction layer overhead
  • Complexity for simple use cases
  • Learning curve for advanced features
  • Smaller community compared to direct backend libraries

Best Use Cases:

  • Complex applications with multiple caching needs
  • SQLAlchemy/ORM-heavy applications
  • Enterprise applications requiring sophisticated caching strategies
  • Applications needing backend flexibility

Performance Comparison Matrix#

Speed (measured, §0.1 and §0.3):#

Librarya hita storenotes
functools.lru_cache0.03 µs0.07 µsin-process, stdlib C
cachetools0.55–1.2 µs0.3–1.4 µsin-process, pure Python; by policy
diskcache4 µs350 µs (18 µs batched)SQLite on disk
redis-py106 µs median113 µsone trip; 7 µs per key pipelined
pymemcache71 µs median68 µsone trip; 2.9 µs per key with get_many
dogpile.cachebackend + 5 µsbackendmemory backend measured

Memory Efficiency:#

LibraryOverheadCompressionPersistence
pymemcacheMinimalNoNo
redis-pyMediumOptionalYes
cachetoolsMinimalNoNo
diskcacheLowOptionalYes
dogpile.cacheMediumBackend-dependentBackend-dependent

Feature Comparison:#

Featureredis-pypymemcachediskcachecachetoolsdogpile.cache
Distributed
PersistentBackend-dependent
ClusteringManualBackend-dependent
DecoratorsManualManual
TTL
LRUManual
MonitoringExtensiveBasicBasicNoneBackend-dependent

Ecosystem Analysis#

Community and Maintenance:#

  • redis-py: Very active, Redis Inc. backing (MIT-licensed; connects to Redis and Valkey alike), extensive documentation
  • pymemcache: Pinterest-maintained, stable, focused scope
  • diskcache: Grant Jenks maintained, regular updates, good documentation
  • cachetools: Thomas Kemmer maintained, stable, minimal changes needed
  • dogpile.cache: Mike Bayer (SQLAlchemy) maintained, enterprise focus

Production Readiness:#

  • redis-py: Enterprise-proven, extensive operational tooling
  • pymemcache: Battle-tested at Pinterest, Wikipedia scale
  • diskcache: Reliable for single-server use cases
  • cachetools: Simple and stable, good for contained use cases
  • dogpile.cache: Enterprise-ready, complex deployment scenarios

Integration Patterns:#

  • redis-py: Often combined with Redis Cluster, Redis Sentinel
  • pymemcache: Typically used with load balancers, consistent hashing
  • diskcache: Standalone or with application-level coordination
  • cachetools: Function-level integration, decorator patterns
  • dogpile.cache: Framework integration, especially with SQLAlchemy

Architecture Patterns and Anti-Patterns#

Multi-Tier Caching:#

# L1: In-memory for hot data
@cachetools.cached(cachetools.TTLCache(maxsize=100, ttl=60))
def hot_data(key):
    # L2: Redis for shared data
    result = redis_client.get(f"shared:{key}")
    if result:
        return json.loads(result)

    # L3: Database for persistent data
    result = database.query(key)
    redis_client.setex(f"shared:{key}", 300, json.dumps(result))
    return result

Cache-Aside Pattern:#

def get_user_profile(user_id):
    # Check cache first
    cached = redis_client.get(f"user:{user_id}")
    if cached:
        return json.loads(cached)

    # Load from database
    profile = database.get_user(user_id)

    # Update cache
    redis_client.setex(f"user:{user_id}", 3600, json.dumps(profile))
    return profile

Write-Through Caching:#

def update_user_profile(user_id, data):
    # Update database
    database.update_user(user_id, data)

    # Update cache immediately
    redis_client.setex(f"user:{user_id}", 3600, json.dumps(data))

Anti-Patterns to Avoid:#

Cache Stampede (Multiple requests regenerating same data):#

# BAD: No protection against simultaneous cache misses
def expensive_operation(key):
    result = cache.get(key)
    if not result:
        result = very_expensive_computation()  # Multiple threads might run this
        cache.set(key, result, ttl=300)
    return result

# GOOD: Use locking or single-flight pattern
import threading
_locks = {}

def expensive_operation(key):
    result = cache.get(key)
    if not result:
        lock = _locks.setdefault(key, threading.Lock())
        with lock:
            result = cache.get(key)  # Double-check
            if not result:
                result = very_expensive_computation()
                cache.set(key, result, ttl=300)
    return result

Cache Invalidation Race Conditions:#

# BAD: Data modification without proper cache invalidation
def update_data(key, new_data):
    database.update(key, new_data)
    # Race condition: cache might be repopulated with old data here
    cache.delete(key)

# GOOD: Atomic operations or versioning
def update_data(key, new_data):
    with database.transaction():
        database.update(key, new_data)
        cache.delete(key)

Selection Decision Framework#

Use redis-py when:#

  • Multi-server application architecture
  • Need pub/sub, transactions, or complex data structures
  • Require persistence and high availability
  • Team has Redis operational expertise
  • Budget allows for a server to run, or a managed SKU

Use pymemcache when:#

  • Maximum caching performance required
  • Simple key-value caching sufficient
  • Distributed caching needed but Redis features unnecessary
  • Cost optimization important (cheaper than Redis)
  • Existing Memcached infrastructure

Use diskcache when:#

  • Single-server deployment
  • Need cache persistence across restarts
  • Zero additional infrastructure desired
  • Development or staging environments
  • File-based caching acceptable performance

Use cachetools when:#

  • Single-process application
  • Function result memoization primary use case
  • Minimal complexity preferred
  • Prototype or development phase
  • No external dependencies allowed

Use dogpile.cache when:#

  • Complex multi-backend caching requirements
  • Heavy SQLAlchemy/ORM usage
  • Enterprise features needed (regions, advanced invalidation)
  • Backend flexibility important for future changes
  • Team has expertise in advanced caching patterns

Technology Evolution and Future Considerations#

Current direction (2026):#

  • Two servers, one client: Valkey (BSD) and Redis (RSALv2 / SSPLv1 / AGPLv3) behind the same redis-py; managed SKUs on AWS and GCP default to Valkey (§0.5)
  • RESP3 by default in redis-py 8 — the transport for server-assisted client-side caching
  • Compiled in-process caches (cachebox, Rust) at 0.28 µs per hit, between the stdlib and cachetools
  • Scan-resistant policies (W-TinyLFU in theine, ARC) for the mixed-traffic case where LRU loses to LFU
  • Full viability analysis in S4

Conclusion#

The caching library ecosystem offers clear specialization:

  1. Redis dominates distributed caching with rich features and proven scalability
  2. Memcached leads pure performance for simple key-value caching
  3. DiskCache excels for single-server persistent caching needs
  4. cachetools provides simplicity for in-process function memoization
  5. dogpile.cache handles complexity for enterprise multi-backend scenarios

Recommended approach: Start with cachetools for immediate gains, evolve to Redis for distributed needs, consider specialized solutions (Memcached, DiskCache) for specific performance or deployment constraints.

S3: Need-Driven

S3 Need-Driven Discovery: Caching Libraries#

Measured: 2026-08-20, for the cache-me floor model (/workshop/cache-me/). Numbers are from S2 §0 (CPython 3.14.7, one machine); the hit rates are deterministic and re-run in any browser at the floor model.

The three questions every persona answers#

  1. Which tier? One process (functools.lru_cache, cachetools), one machine across restarts (diskcache), or many machines (redis-py against Valkey or Redis, pymemcache against memcached). The tier is set by where the readers of the cache run, and it decides before the library does.
  2. What does a miss cost? A hit is cheap on every tier — 0.03 µs in-process, 4 µs on disk, 50–100 µs across a socket. The miss pays for the function and the store, and on diskcache the store is 350 µs on a real disk. The cache pays when the hit rate clears store ÷ (function + store − hit); the floor model’s third panel computes it.
  3. What is the access pattern? The hit rate belongs to the traffic, not the library. On a stable hot set every policy is within a few points; when the hot set moves LRU beats LFU by 16; on a loop bigger than the cache LRU and FIFO score exactly zero.

Use Case 1: Several application servers, one answer#

Who: A team running a web API on more than one process or host, where the same expensive result — a rendered fragment, a computed listing, a third-party lookup — is wanted by every instance.

Why it is a caching decision: In-process caches fill once per instance and cannot be invalidated together. The requirement is a cache the instances share, so a miss on one becomes a hit on all.

# Requirements
# - shared across processes and hosts
# - a time-to-live, so a stale entry expires without anyone remembering to delete it
# - a round trip cheap next to the thing being cached
# - one server to run, or one managed service to pay for
LibraryFitMeasured
redis-py → Valkey or Redis✅ The answerGET 45 µs min / 106 µs median over a socket; 7 µs per key pipelined
pymemcache → memcached✅ If a cache is all you want14 µs min / 71 µs median; leaner client, same median
diskcache❌ One machine only
cachetools / lru_cache❌ One process only
dogpile.cache (Redis backend)✅ Adds the stampede lockbackend + ~5 µs

Answer: redis-py with SETEX, against whichever of Valkey and Redis the license and the cloud bill favor (S2 §0 — they are the same number behind the same client). Pipeline anything that fetches more than one key. memcached is the right pick when the team wants nothing else from the server and wants to say so.

Use Case 2: One machine, expensive results that must survive a restart#

Who: A data engineer or analyst whose job recomputes costly intermediate results — query results, feature tables, downloaded artifacts — on one box, and restarts the process often.

Why it is a caching decision: An in-process cache dies with the process; a server is a second thing to run for a one-machine job. The requirement is persistence without infrastructure.

# Requirements
# - survives a restart
# - handles large values (100 KB – 10 MB)
# - no server
# - a miss is a query that takes milliseconds to minutes, so store cost is tolerable
LibraryFitMeasured
diskcache✅ The answerget 4 µs; set 350 µs on disk, 18 µs inside one transact()
redis-py✅ But a server to run106 µs median per trip
cachetools❌ Lost on restart
pymemcache❌ Lost on restart, server to run
dogpile.cache (file backend)✅ Workable

Answer: diskcache. Its memoize() decorator is 12 µs per hit, and with a 10 ms function the cache breaks even at a 4% hit rate. Wrap bulk loads in with cache.transact(): — the single-write cost is SQLite’s transaction, and one transaction around 200 writes divides it by twenty.

Use Case 3: Sessions, counters and rate limits across instances#

Who: The same multi-instance web team as Use Case 1, but the data is small, written often, and correctness matters: a session token, a login-attempt counter, a per-user rate limit.

Why it is a caching decision: This is not caching in the memoisation sense — it is shared ephemeral state. The requirement is an atomic increment and an expiry, across hosts, in well under a millisecond.

# Requirements
# - INCR / EXPIRE atomically, so two instances cannot both admit the 101st request
# - TTL per key
# - sub-millisecond round trip
# - survives an instance restart (not necessarily a server restart)
LibraryFitMeasured
redis-py → Valkey or Redis✅ The answerINCR is one 100 µs trip; EXPIRE can ride the same pipeline
pymemcache → memcached✅ For sessions; incr exists, fewer atomic combinationssame trip
diskcache❌ One machine
cachetools / lru_cache❌ One process

Answer: redis-py. The data structures are the reason — a counter with an expiry, a set of active sessions, a sorted set of recent attempts — and the survey’s finding that “Redis wins on what is not caching” is this use case.

Use Case 4: Memoising a function in one process#

Who: Any Python developer with a pure-enough function called repeatedly with the same arguments — parsing, a lookup table, a computed property.

Why it is a caching decision: The first reach, and the one most often over-engineered.

# Requirements
# - a decorator
# - keys built from the arguments
# - no dependency, or at most one
# - optionally: a time-to-live, a size in bytes, a way to inspect and clear
LibraryFitMeasured (hit)
functools.lru_cache (stdlib)✅ Start here0.03 µs — a dict lookup
cachetools✅ When you need TTL, sizing by weight, or a non-LRU policyLRU 0.55 µs, LFU 0.8 µs, TTL 1.2 µs
diskcache.memoize✅ When the value must outlive the process12 µs
cachebox (Rust)✅ The in-between — not in the survey’s core, see S40.28 µs
redis-py / pymemcache❌ A network trip for a local function100 µs

Answer: functools.lru_cache until one of three things is true — the entries must expire on a clock, the cache must be bounded by memory rather than count, or a scan-heavy pattern wants a policy other than LRU. Then cachetools, and accept a microsecond per hit, which is nothing against any function worth wrapping. If the function costs under ~10 µs, do not cache it: the decorator is the cost.

Use Case 5: Development and test environments#

Who: A developer who wants production-shaped caching behavior on a laptop and in CI without installing a server.

Why it is a caching decision: The cache’s tier in production is often the network; in development the same code should run against something with no daemon and the same API shape.

LibraryFit
diskcache✅ No server, persists between runs, inspectable on disk
cachetools✅ For unit tests; lost between runs
dogpile.cache✅ The one that swaps backends by configuration — memory locally, Redis in production
redis-py✅ With a local Valkey; fine where the team already runs one

Answer: diskcache for a persistent local stand-in; dogpile.cache if the codebase already wants one API over two backends. A developer who can run Valkey locally loses nothing by doing so.

Who: A platform team at a company whose open-source policy bans AGPL outright, or an ISV that ships its product into customer data centers and cannot carry source-available terms (RSALv2 / SSPLv1) on a component. Both are common; neither existed as a caching problem before 2024.

Why it is a caching decision now: Redis 8 is offered under RSALv2 or SSPLv1 or AGPLv3. Each of the three fails a different policy. The team still wants the Redis wire protocol, the redis-py code they already have, and a managed option on their cloud.

# Requirements:
# - OSI-approved permissive license on the server (BSD / Apache / MIT)
# - Existing redis-py code runs unchanged
# - Managed offering on AWS or GCP
# - A way back if the fork stalls

Answer: Valkey (BSD-3-Clause) behind the existing redis-py client. On AWS, ElastiCache for Valkey is also the cheaper SKU (20% node-based, 33% serverless); on GCP, Memorystore for Valkey. Measured behind redis-py, Valkey 9.1.1 and Redis 8.10.1 are the same number (106 and 110 µs median). The “way back” is real only while the data stays on Redis ≤ 7.2 semantics — Valkey reads those RDB files, Redis 7.4+ files it does not, and the reverse move from Valkey 8/9 features (hash-field expiry, cluster databases) is not guaranteed either. Write that down in the decision record.

The mirror persona: a team already on Redis 7.4+ or 8 using Stack modules (JSON, search, vector sets). For them the license question is settled by inertia — leaving means a key-by-key copy over the wire and re-homing modules onto valkey-json / valkey-search, which are younger (1.x, 2025–2026). Staying on Redis under AGPLv3 is usually fine for an internal service that does not distribute the server; it is the ISV case that forces the move.

Use Case 7: The batch job that reads more than fits#

Who: Anyone running a nightly or hourly job that walks a table, a corpus or a key space larger than any cache it could be given, once per pass.

Why it is a caching decision: It looks like the ideal candidate — the same keys, every run — and it is the one case where a cache does nothing. Measured on a loop over 10,000 keys: LRU and FIFO score 0.0% at 1%, 5%, 10%, 25% and 50% of the keys. Each key is evicted one step before it comes round again.

What helpsWhat does not
A cache at least as large as the loop — then every pass after the first is all hitsA better LRU
No cache, and a faster loopA TTL
LFU, by accident — ties keep a fixed subset, 6.7% at 10% (the offline optimum)A bigger LRU, up to half

Answer: Size the cache to the working set or do not add one. This is the persona the floor model’s second panel was built to show.

Constraint-Based Decision Matrix#

By tier#

Readers of the cacheUseThen consider
one processfunctools.lru_cachecachetools for TTL / weight / policy
one machine, across restartsdiskcachebatch writes in transact()
many machinesredis-py → Valkey or Redispymemcache if the server should do nothing else; dogpile.cache for the stampede lock

By what a miss costs#

The wrapped function costsIn-process pays at hit rate ≥diskcache (disk write) pays at ≥one socket trip pays at ≥
10 µs14% (TTLCache)nevernever
100 µs1.4%82%52%
1 ms0.1%26%10%
10 ms~03.4%1.1%
1 s~00.04%0.01%

Computed with core.payoff() from the measured hit and store costs (TTLCache 1.2 / 1.4 µs; diskcache 4 / 350 µs; redis-py 106 / 113 µs median). The floor model recomputes it on your machine.

By access pattern (10% of keys cached, 30,000 requests over 10,000 keys)#

PatternLRULFUFIFOrandomceiling
stable hot set67%68%63%64%79%
hot set that moves66%49%62%61%76%
hot set + scan49%53%45%45%64%
no favourites10%10%10%10%38%
loop over everything0%7%0%0%7%

Risk Assessment by Requirements#

RiskWhere it bitesMitigation
The cache makes things slowerfast functions on diskcache or the networkcompute the break-even first; the table above
Zero hitsany loop larger than the cachesize to the working set or remove the cache
Stale popularityLFU on traffic whose favourites moveLRU — it recovers
Stampedea hot key expiring under loaddogpile.cache’s lock, or a single-flight guard
Server gonethe network tiertreat a cache miss on connection error as a miss, not an exception
LicenseRedis 7.4+ / 8 in a distributed productValkey; S2 §0 and Use Case 6

Conclusion#

No single library meets every need, and the reason is structural: the tiers are different problems. The sequence that fits most teams is functools.lru_cachecachetools when it needs a clock or a weight → diskcache when it must survive a restart → redis-py when a second machine needs the same answer — each step taken when the previous tier’s readers are not the only readers. And before any of them: know the access pattern, because on a loop the best library in the survey scores nothing.

S4: Strategic

S4 Strategic Discovery: Caching Libraries#

Date: 2026-08-20. Who maintains each library, what has moved in the category, what is coming, and when each choice stops paying. Maintenance signals are GitHub and PyPI, read 2026-08-20.

Viability, library by library#

LibraryVersion, dateLicenseSignalsVerdict
functools.lru_cachestdlibPSFC implementation since 3.5The baseline. Cannot be abandoned, cannot expire entries.
cachetools7.1.7MIT2.8k stars; pushed 2026-08; one maintainer (Thomas Kemmer) for 12 years; 339M downloads/moHealthy. The API has been stable across majors; 7.x dropped Python 3.9. Single-maintainer risk is real and has never materialised.
diskcache5.6.3 (2023-08)Apache-2.02.9k stars; last push 2024-08; 75 open issues; 37M downloads/moWorks, widely installed, and quiet for two years. Pure Python on SQLite, so there is nothing to bit-rot against the interpreter. Watch, do not avoid.
redis-py8.1.0 (2026-07-30)MIT13.6k stars; pushed daily; Redis Inc. staff; 275M downloads/moThe most active client in the category. RESP3 by default since 8.0. Works against Valkey unchanged.
valkey-py6.1.1 (2025-08)MIT280 stars; 6.2.0 at rc2 since 2026-03; 5.1M downloads/moA fork that tracks redis-py slowly. Use only where the dependency’s name is the objection.
valkey-glide2.5.1 (2026-08-11)Apache-2.0778 stars (monorepo); AWS and GCP named backers; 1.0M downloads/moThe client built for Valkey first: Rust core, asyncio, cluster-aware, OpenTelemetry. Lists Redis only through 7.2. Measured 5× slower per awaited op than redis-py on this machine; its case is concurrency, not single-call latency.
pymemcache4.0.0 (2022-10)Apache-2.0839 stars; pushed 2025-07; Pinterest; 3.3M downloads/moFinished software. The protocol has not changed; neither has the client.
python-memcached1.62PSF470 stars; pushed 2025-09The older client. pymemcache is the maintained recommendation.
dogpile.cache1.5.0MIT295 stars; pushed 2026-08; Mike Bayer (SQLAlchemy); 2.5M downloads/moSmall, steady, and the only one with a stampede lock. Not a cache — a front end to one.

Two entrants the survey’s core does not carry but a reader will meet:

  • cachebox (6.2.5, MIT, Rust, 427 stars, pushed 2026-08): compiled wheels for CPython 3.10+, measured 0.28 µs per hit — half cachetools, ten times lru_cache. Not in Pyodide. It is what you reach for when cachetools’ microsecond is measurable in your profile, which is rare.
  • theine (2.0.0, BSD-3, 429 stars, pushed 2025-11): W-TinyLFU, the scan-resistant admission policy cachetools lacks. Measured elsewhere, not here; its claim is hit rate on mixed traffic, which the floor model’s “hot set plus scan” pattern is the test for.

What moved since 2025#

The server split and re-joined. Redis 7.4 left BSD for RSALv2 / SSPLv1 in March 2024; the Linux Foundation forked 7.2.4 as Valkey the same month; AWS re-priced ElastiCache for Valkey 20–33% below Redis OSS in October 2024; Redis 8 added AGPLv3 in May 2025 and says it intends to keep it. Measured behind the same client, the two servers are the same number. The decision moved from engineering to procurement, and it is one-way past Redis 7.2 because the RDB format diverged. The dates, versions and clients are in S2 §0.5.

RESP3 became the default on the wire (redis-py 8.0, 2026). The visible effect for a cache is none; the invisible one is that client-side caching — the server telling the client which keys it holds have changed — rides on RESP3’s push messages. That is the mechanism by which the network tier can acquire an in-process tier in front of it without the application managing two caches.

The Python clients’ throughput ceiling is the round trip. The figures in circulation — “100,000+ ops/sec” for redis-py, “200,000+” for memcached — are server figures. A single synchronous Python client does 3,000–10,000 round trips a second on a socket (106 µs median here), and 100,000+ only when pipelined (7 µs per key). The measurements are S2 §0.3; the practice is “never fetch two keys in two trips”.

The standard library got cheaper to beat and harder to justify beating. lru_cache is a dict lookup. cachetools’ microsecond buys TTL, weight and policy; cachebox’s 0.28 µs buys a compiled dependency. For almost every function worth caching, neither cost is visible.

What is coming#

  • Scan-resistant policies in the mainstream. W-TinyLFU (Caffeine’s policy, theine’s) and ARC solve the pattern on which LRU loses to LFU — a hot set with a scan through it, 49% against 53% at 10% here. cachetools has no such policy and has not announced one; the gap is small on most traffic and decisive on some.
  • Compiled in-process caches (cachebox, and the Rust ecosystem generally) will keep narrowing the gap to lru_cache while adding the features it lacks. Expect them to remain optional.
  • Server-assisted client caching (RESP3 tracking) as the default two-tier architecture, with the client library managing invalidation. redis-py ships the machinery; the defaults do not turn it on.
  • Valkey and Redis diverging in features while staying compatible on the cache path. Valkey 9 added cluster databases, atomic slot migration and hash-field expiry; Redis 8 added vector sets and bundled the Stack modules. GET/SET/SETEX/EXPIRE/pipelines are identical and will stay so — that is the compatibility both projects are paid to keep.
  • diskcache’s successor question. Two quiet years on a 37M-download library is not a crisis — SQLite does not move underneath it — but a maintained fork or a replacement built on the same idea is the likely shape of the next five years.

When each choice stops paying#

You choseIt stops paying when
functools.lru_cacheentries must expire on a clock, or memory must be bounded by size rather than count
cachetoolsthe function you wrap costs less than ~10 µs — the decorator is then the cost
cachetools LFUthe favourites move; it keeps counting the old ones (49% against LRU’s 66% on the moving hot set)
diskcachethe function costs under ~1 ms and the hit rate is under ~30%: a miss is a 350 µs write
diskcache (unbatched)bulk loads; use transact() — 18 µs per write instead of 350
redis-py / pymemcachethe readers are all one process; or you fetch N keys in N trips
valkey-glideyou might go back to Redis 8 (not in its engine table), or the workload is one call at a time
valkey-pyredis-py would do — it will be a release behind
dogpile.cachenothing in the system needs the lock or the backend swap
any cache at allthe access pattern is a loop larger than the cache — 0% at every size up to half

Risks#

RiskLikelihoodEffectHedge
Redis re-licenses againlow (Redis states it intends to keep AGPLv3)procurement, not coderedis-py speaks to both; stay on ≤ 7.2 RDB semantics if the exit matters
Valkey stallslow (LF governance, seven launch backers, 9.x cadence)none on the cache pathsame
diskcache abandonedmedium over five yearsnone immediate — SQLite is stablevendor the file; it is pure Python
cachetools single maintainerpresent, unchanged for 12 yearsa fork if it happenedthe API is small; a fork is a weekend
A cache that hurtscommon, never measuredslower code with more moving partsthe break-even table in S3; the floor model’s third panel

Strategic recommendation#

Adopt in the order of tiers and measure before each step. In-process with the standard library first; cachetools when a clock or a weight is needed; diskcache when the value must survive the process; redis-py when a second machine needs the answer — against Valkey unless the team is already on Redis 7.4+ with Stack modules, in which case staying is cheaper than the key-by-key copy out. Treat the hit rate as a property of the traffic to be measured, not a property of the library to be assumed, and put a number on the miss before adding the cache.


Measured: 2026-08-20

Published: 2025-01-28 Updated: 2026-08-20