Back to all projects
FlipCache backdrop
2024 → PresentOpen-source on PyPI4 min read0
FlipCache logo

FlipCache

A lightweight Python cache that keeps hot keys in-process, syncs them to Redis, and ships FIFO/LRU utilities for both async and threaded workloads.

Pythonredis-pyasyncio
Read acceleration

~1300× vs raw Redis GET

Release

v1.3 (PyPI)

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 OrderedDict reads 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:

Bash
pip install flipcache

Architecture

FlipCache high-level architectureView

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.

Python
value = cache["user:42"]

Under the hood, the lookup works like this:

  1. Coerce the key to the configured key type.
  2. Return from the local OrderedDict when present.
  3. On miss, read from Redis using the cache prefix.
  4. Decode the value when value_type requires it.
  5. Rehydrate the local tier without exceeding local_max.
  6. 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.

Python
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:

Python
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 provide value_encoder and value_decoder.

Custom codecs are the escape hatch for dataclasses, binary payloads, compact formats, or project-specific serialization rules.

Python
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.

Python
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:

  • FIFODict evicts the oldest inserted item.
  • LRUDict evicts the least recently used item.
  • ThreadSafeFIFODict and ThreadSafeLRUDict wrap operations with locks.
  • AsyncSafeFIFODict and AsyncSafeLRUDict expose async-safe methods for coroutine-heavy code.
Python
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.

ScenarioMean (s)Std Dev
redis_set0.2520.013
flipcache_set0.2420.003
redis_get22.9860.518
flipcache_get0.01720.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.

Need a similar system?

If you are building a bot, automation workflow, or platform feature, I can help with architecture and implementation.

Start a project

More projects

Related systems, decisions, and outcomes.