cachefence
Cache-aside for Redis without the stampede.
When a hot cache key expires, naive cache-aside lets every concurrent request miss at the same instant and pile onto your database to rebuild the same value. That's a cache stampede (a.k.a. thundering herd), and it's one of the most common ways a cache makes things worse under load.
cachefence stops it:
500 concurrent requests hit a cold key (each DB query takes 50ms)
naive cache-aside DB hits: 500
with cachefence DB hits: 1
Same workload, one extra import: 500 database queries become 1.
Install
pip install cachefence
Requires Python 3.11+ and a Redis server (4.2+).
Usage
from redis.asyncio import Redis
from cachefence import CacheFence
redis = Redis()
cache = CacheFence(redis)
async def get_user(user_id: int) -> dict:
return await cache.get_or_set(
key=f"user:{user_id}",
ttl=60, # fresh for 60 seconds
recompute=lambda: load_user_from_db(user_id),
)
recompute can be sync or async. It runs at most once per refresh, no matter how
many requests arrive together. Invalidate manually when the underlying data
changes:
await cache.invalidate(f"user:{user_id}")
How it works
cachefence layers two mechanisms so a key almost never goes cold and a cold key is never rebuilt more than once:
-
Probabilistic early refresh (XFetch). Each read rolls a weighted dice; as the key nears expiry, one lucky request is nudged to refresh it ahead of time while everyone else keeps serving the still-valid cached value. The weighting uses how long the last recompute took, so expensive keys refresh earlier. Based on Vattani, Chierichetti & Lowenstein, "Optimal Probabilistic Cache Stampede Prevention" (VLDB 2015).
-
Distributed rebuild lock. On a true miss, workers race for a short-lived Redis lock. The winner rebuilds; the rest wait briefly and pick up the fresh value the moment it lands, with a bounded fallback so a crashed rebuilder never hangs requests forever.
The lock is released with a compare-and-delete (Lua when the server supports it,
an optimistic WATCH/MULTI transaction otherwise) so a worker can never delete
a lock it no longer owns.
Configuration
cache = CacheFence(
redis,
beta=1.0, # XFetch aggressiveness; higher = refresh earlier
lock_timeout=10.0, # seconds before a rebuild lock auto-expires
wait_for_lock=5.0, # max seconds a waiter blocks before rebuilding itself
namespace="app:", # optional key prefix
)
Custom serialization (default is JSON):
import pickle
cache = CacheFence(redis, serializer=pickle.dumps, deserializer=pickle.loads)
# serializer returns bytes, deserializer takes bytes
A note on connection pools
Under a genuine burst (hundreds of simultaneous coroutines), the default
redis-py pool can raise MaxConnectionsError because waiters don't block for a
free connection. Use a blocking pool sized for your concurrency:
from redis.asyncio import BlockingConnectionPool, Redis
pool = BlockingConnectionPool(max_connections=30, timeout=15)
redis = Redis(connection_pool=pool)
Run the demo
git clone https://github.com/bourne44/cachefence
cd cachefence
pip install -e ".[test]"
python examples/stampede_demo.py
Development
pip install -e ".[test]"
pytest
The test suite includes a 100-way concurrent-miss test asserting the recompute runs exactly once — the core guarantee of the library.
License
MIT
Metadata
Release files for cachefence 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| cachefence-0.1.0.tar.gz | 9.1 kB | Without provenance | Details for cachefence-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cachefence-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for cachefence-0.1.0-py3-none-any.whl |
Total release size: 17.7 kB
Release files / cachefence-0.1.0.tar.gz
| Download URL | cachefence-0.1.0.tar.gz |
|---|---|
| Size | 9.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d1b741e63d5cf7eb5f2cb2a2412c9b5ac9dfe7877ac078f2f83c6dcd1cb35663
|
|
BLAKE2b-256 checksum How to use checksums |
f0dec7304401279926be479a586c47cb9eea2c11096378831a8c93379e3cb043
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|
Release files / cachefence-0.1.0-py3-none-any.whl
| Download URL | cachefence-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d1f61a99145fb63821de3657d3cc203ce3277caf08965333c6d8e16d1f70305
|
|
BLAKE2b-256 checksum How to use checksums |
19f1baf6e5928d26fc9e27c0be8893e3b1c1cb4c51ee1166bc76ded094e8f03f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|
Security for cachefence 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.