Workshop · floor model for Survey of Software 1.047

cache me if you can

A cache hit in the standard library costs the same as a dict lookup. The cache most people install costs twenty times that — and neither number decides anything, because a cache is paid for by its misses. Below: what a hit and a miss cost on each tier, which eviction policy earns the hit rate on the access pattern you actually have, and the hit rate under which the cache makes your code slower. Measured in this tab.

Everything runs here. Press start, then change the access pattern, the cache size, and the cost of the function you are wrapping.

Plain language. Switch to Engineering for versions, byte counts and the measurements behind each claim.

The whole answer, first

Three tiers of cache, one question each. The numbers were measured for the survey on one machine; the panels below re-measure them on yours.

CPython 3.14.7, aarch64, single thread, best-of under a budget; one hit on the cheapest possible wrapped function, so the overhead is the whole number. The method and the full tables are in S2 §0 of the survey; the bench imports this page's core.py.

in the process

0.03 µs

a hit on functools.lru_cache — the same as a dict lookup. cachetools LRU: 0.5 µs; its TTLCache: 1.2 µs. Twenty to forty times more, and still nothing next to anything worth caching.

on the machine

4.5 µs

a hit on diskcache — SQLite, survives a restart, no server. A miss writes: 200–450 µs per set on disk, 18 µs inside one transaction.

on the network

106 µs

a redis-py round trip to Valkey or Redis with the network taken out (unix socket, median; 45 µs at best). This machine's TCP loopback makes it 320 µs; a real network adds its own. Hit and miss cost the same trip; a pipeline makes it 7 µs a key.

the policy

0%

what LRU — everyone's default — scores on a loop over more keys than fit, at any cache size up to half. On a stable hot set it is within a point of LFU; when the hot set moves it beats LFU by sixteen.

The tier decides before the library. Inside one process, the standard library's lru_cache is the fastest cache in Python and the only one you need until you want a time-to-live, a size in bytes, or to evict by hand — then cachetools, for a microsecond. On one machine across restarts, diskcache — but batch the writes. Across machines, redis-py against Valkey or Redis, which are the same server with different licences. And before any of it: the hit rate is a property of your access pattern, not of the library, and the second panel lets you find out what yours is.

The rule of this page: every figure below is measured on your machine while you wait, by the Python libraries themselves running in this tab. Where a number came from research instead, it says so in orange.

Press start to download CPython 3.14 (WebAssembly). First time only, then cached.

What a hit costs — and what a miss costs

Each cache wraps the cheapest function there is, k + 1, so the bar is the cache's own overhead. Left: a hit. Right: storing a new value, which is what every miss has to do on top of running the function.

1,000 keys, warmed, then 1,000 calls per batch, best-of under a budget (the browser clock clamps to 100 µs, so batches are calibrated to ≥ 5 ms first). Log scale. diskcache is SQLite on this tab's in-memory filesystem — its read cost is representative, its write cost is not, because nothing here is fsynced; the disk figure is in orange.

a hit

Run the page first.

a store

Why the standard library wins by twenty times: lru_cache is C — a hash lookup and a pointer move. cachetools is Python — a decorator, a key built from your arguments, a method call into a cache class, and for TTLCache a clock read and an expiry sweep. That is what you buy: a time-to-live, a size measured however you like, caches you can inspect and clear, five eviction policies. It costs a microsecond. A database query costs a thousand of those; an HTTP call ten thousand. If the function you are wrapping costs less than about ten microseconds, the answer is not to cache it.

Which policy earns the hit rate

The same requests replayed through each of cachetools' eviction policies, plus the standard library's lru_cache and the best any policy could possibly do if it could see the future. Pick the shape of your traffic and how much of it fits.

30,000 requests over 10,000 keys, deterministic (seed 7), so the numbers are identical here and in the survey's native run. Each policy is cachetools' own class — LRUCache, LFUCache, FIFOCache, RRCache — replayed with __getitem__ so the order is updated on a hit. The ceiling is Belady's offline MIN. "Hot set" is Zipf with s = 1 over a shuffled key space.

Run the page first.

What the patterns say. On a stable hot set, every sensible policy lands within a few points of each other and a cache holding one key in a hundred already catches two requests in five. When the favourites move, LRU follows them and LFU does not — it keeps counting yesterday's hits. On a loop that does not fit, LRU and FIFO evict each key exactly one step before it is needed again, every time, at every size up to half; the only policies that score are the ones that refuse to follow the loop. With no favourites at all, the hit rate is the cache size and no policy can change it. Measure your pattern before choosing a policy — and if you cannot, LRU is the default for the right reason: it is the one that recovers.

Does it pay?

Take the hit rate from the pattern and size you chose above, the hit and store costs measured on this machine, and the cost of the function you want to wrap. The table says what you would actually save — and the hit rate under which each tier makes your code slower.

mean time with cache = h·hit + (1 − h)·(fn + store); pays when h > store ÷ (fn + store − hit). The network rows use the survey's figures (orange): a unix socket is the tier's best case — no network — and this machine's TCP loopback its second-best.

100 µs

1 µs is a dict lookup; 100 µs a local database hit; 10 ms a query that does work; 1 s a model or a remote API.

Run the page first.

The miss is the price. A hit on diskcache costs a few microseconds; the write that follows a miss costs hundreds on a real disk. So for a function that takes 100 µs, diskcache needs more than eighty hits in a hundred just to break even — and for one that takes 10 ms, four in a hundred. Caching pays for slow functions and punishes fast ones, and the line between them is a number you can now read off.

What could not be measured here

From research, not measured in this tab

The network tier — a server is not a thing a browser tab can start. Measured natively for the survey on 2026-08-20: one Python client against Valkey 9.1.1, Redis 8.10.1 and memcached 1.6.38, one GET at a time, then 100 in a pipeline — over a unix socket (no network at all) and over TCP loopback.

Redis or Valkey — the same wire protocol, the same client, and since 2024 two licences: Redis 8 is RSALv2, SSPLv1 or AGPLv3; Valkey (the Linux Foundation's fork of Redis 7.2) is BSD-3-Clause. Valkey reads Redis 7.2 data files and not 7.4+ ones, so the door is one-way. The survey's S2 §0 has the dates and versions.

pymemcache — memcached does one thing. Measured against the same loopback, the difference between a memcached round trip and a Valkey round trip is in the table above; the speed you feel is the network's, not the server's.

dogpile.cache — a front end over any of the above (memory, file, Redis, memcached) with the one thing none of them give you: a lock so that one miss recomputes and the others wait, instead of a stampede. Not timed; its cost is the backend's plus a Python call.

Start your build

Take what you just measured and hand it to whichever AI you use — the costs on your machine, the pattern and hit rate you chose, and a brief for what to do next.

Clipboard envelope — no vendor deep link, no API key, no dependency on anyone's roadmap.

Run the page first.

Open the lab

The measurement is one file, core.py, fetched by this page and imported by the survey's bench. uvx marimo edit a notebook that imports it, or read it — it is short.

cache me if you can is a floor model for Survey of Software 1.047 — Caching Libraries, verified 2026-08-20. Libraries run under Pyodide (CPython 3.14 compiled to WebAssembly) in this tab; nothing is sent anywhere. Timings here are WebAssembly timings — roughly a third of native speed, same shape. Measurements are from your machine except where marked orange.

Made by Ivan Schneider · Model Citizen Developer