FlipCache
Why I Built It
Redis is reliable and shared across processes, but every read still crosses a network boundary. A plain Python dictionary is extremely fast, but it disappears when the process restarts and cannot be shared between workers.
FlipCache sits between those two extremes. Hot keys stay in process for fast reads, while Redis remains the durable backing store for misses, restarts, and cross-process access.
What It Offers
- Hybrid caching - local
OrderedDictreads first, Redis second. - Namespaced Redis keys - every cache instance uses its own prefix.
- Typed values - built-in handling for
str,int,json, and custom codecs. - TTL controls - Redis expiry can be configured per cache.
- Read refreshes - hot keys can extend their Redis expiry on access.
- Standalone helpers - FIFO/LRU dictionaries are available outside Redis-backed caching.
FlipCache is published on PyPI as FlipCache 1.3 and can be installed with:
pip install flipcache
Architecture
View
The package has one main path and two supporting surfaces. Application code talks to a mapping-style FlipCache object. That object checks local memory first, uses encoders/decoders when values cross the Redis boundary, and keeps Redis as the shared persistent layer. The FIFO/LRU helpers are separate utilities for cases where the caller only needs bounded in-memory eviction.
Read Path
Reads are optimized for the common case: the key is already hot in the local tier.
value = cache["user:42"]
Under the hood, the lookup works like this:
- Coerce the key to the configured key type.
- Return from the local
OrderedDictwhen present. - On miss, read from Redis using the cache prefix.
- Decode the value when
value_typerequires it. - Rehydrate the local tier without exceeding
local_max. - Optionally refresh the Redis expiry for frequently accessed keys.
That is why warmed reads are much faster than pure Redis reads: the request stops at process memory instead of round-tripping through Redis.
Write Path
Writes update both tiers. The local tier gives fast subsequent reads, while Redis keeps the value available after restarts or from another worker.
cache["user:42"] = {"hits": 1, "plan": "pro"}
When a value is written, FlipCache stores it locally, encodes it if needed, writes it to Redis with the configured TTL, and evicts the oldest local item if the cache exceeds local_max.
Usage Example
This is the shape I would use for a small analytics or bot-state cache:
from redis import Redis
from flipcache import FlipCache, et
cache = FlipCache(
"analytics",
local_max=256,
expire_time=et.FIFTEEN_MINUTES,
value_type="json",
value_default={"hits": 0},
refresh_expire_time_on_get=True,
redis_protocol=Redis(decode_responses=True),
)
payload = cache["user-42"]
payload["hits"] += 1
cache["user-42"] = payload
The value_default branch is useful for counters and profile-like state. Missing keys can materialize a default value instead of raising a KeyError, and the default is then written back through the same cache path.
Serialization Layer
FlipCache treats Redis as a byte/string boundary and keeps Python values comfortable on the application side.
value_type="str"stores simple strings.value_type="int"decodes Redis values back into integers.value_type="json"uses JSON for dictionaries, lists, and other JSON-safe payloads.value_type="custom"lets the caller providevalue_encoderandvalue_decoder.
Custom codecs are the escape hatch for dataclasses, binary payloads, compact formats, or project-specific serialization rules.
cache = FlipCache(
"shapes",
key_type="int",
value_type="custom",
value_encoder=encode_shape,
value_decoder=decode_shape,
value_default=Shape(),
)
Expiry Behavior
TTL is controlled by expire_time. If it is set, writes use Redis expiry so stale keys eventually disappear from the backing store.
There is one important tradeoff: local memory can return a value that Redis would already consider expired. If exact expiry matters more than speed, set local_max=0 so every read goes to Redis.
expiring_cache = FlipCache("expiring", local_max=0, expire_time=5)
For read-heavy workloads where hot keys should remain alive, refresh_expire_time_on_get=True extends the Redis TTL whenever the key is read through FlipCache.
Eviction Helpers
The package also ships bounded dictionaries for cases that do not need Redis at all:
FIFODictevicts the oldest inserted item.LRUDictevicts the least recently used item.ThreadSafeFIFODictandThreadSafeLRUDictwrap operations with locks.AsyncSafeFIFODictandAsyncSafeLRUDictexpose async-safe methods for coroutine-heavy code.
from flipcache import LRUDict
recent = LRUDict(max_items=3)
recent["a"] = "Alpha"
recent["b"] = "Beta"
recent["c"] = "Gamma"
_ = recent["a"]
recent["d"] = "Delta" # evicts "b"
These helpers are small, but they make the package useful even when Redis is not part of the current code path.
Benchmarks
The bundled benchmark script compares pure Redis access with FlipCache's hybrid mode using 1,000 keys and a local tier capped at the same size.
| Scenario | Mean (s) | Std Dev |
|---|---|---|
redis_set | 0.252 | 0.013 |
flipcache_set | 0.242 | 0.003 |
redis_get | 22.986 | 0.518 |
flipcache_get | 0.0172 | 0.000 |
The write path stays close to Redis because it still persists values to the backing store. The read path is where the design pays off: once the local tier is warm, repeated reads are served from process memory.
Current Status & Next Steps
FlipCache is public, MIT licensed, and available on PyPI. The current PyPI release is 1.3, released on May 23, 2025.
The useful next work is less about proving the idea and more about tightening the package:
- Add automated tests around TTL refresh, custom codecs, and eviction helpers.
- Expand async Redis support so event-loop applications can use the same model without adapters.
- Publish more structured docs beyond the README examples.
- Keep the benchmarks reproducible as the implementation evolves.





