Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

redis_func_cache

python-package codecov readthedocs pypi-version

A Python library that provides decorators for caching function results in Redis, supporting multiple serialization formats and caching strategies, as well as asynchronous operations.

Introduction

redis_func_cache is a Python library that provides decorators for caching function results in Redis, similar to the caching functionality offered by the standard library. Like the functools module, it includes useful decorators such as lru_cache, which are valuable for implementing memoization.

Unlike in-process caches such as the standard library's functools.lru_cache — which are private to a single Python process — this library uses Redis as a distributed cache backend: a result computed once is shared by every process and machine behind your application, and survives restarts and deploys. Decorated functions keep their ordinary look and feel; the distributed part is handled by the library:

  • Shared across processes and hosts — one cache for all workers, no per-process duplication.
  • Redis Cluster support — dedicated policies pin each cache's keys to one hash slot; cache atomically on a cluster out of the box.
  • Atomic operations — every cache read/write is a single Lua script executed atomically by Redis, safe under high concurrency.
  • High availability — the cache inherits the availability of your Redis deployment (replication, Sentinel, ...): no new infrastructure to operate.
  • Multiple eviction policies — LRU, FIFO, LFU, RR, ... (Refer to Cache Replacement Policies on Wikipedia for more details.)

Here is a simple example:

  1. First, start up a Redis server at 127.0.0.1:6379, e.g.:

    docker run -it --rm -p 6379:6379 redis:alpine
    
  2. Then install the library in your Python environment:

    pip install redis_func_cache
    
  3. Finally, run the following Python code:

    import asyncio
    from time import time
    import redis.asyncio as aioredis
    from redis_func_cache import LruTPolicy, RedisFuncCache as Cache
    
     # Create a redis connection pool (simple example)
     pool = aioredis.ConnectionPool.from_url("redis://")
     # Preferred: provide a factory for production/concurrent use
     factory = lambda: aioredis.Redis.from_pool(pool)
    
     # Create an LRU cache. Note: policy must be an instance and we prefer a factory.
     cache = Cache(__name__, LruTPolicy, factory=factory)
    
     # Decorate a function to cache its result
     @cache
     async def a_slow_func():
         t = time()
         await asyncio.sleep(10)  # Sleep to simulate a slow operation
         return f"actual duration: {time() - t}"
    
     with asyncio.Runner() as runner:
         t = time()
         r = runner.run(a_slow_func())
         print(f"duration={time() - t}, {r=}")
    
         t = time()
         r = runner.run(a_slow_func())
         print(f"duration={time() - t}, {r=}")
    

The output should look like:

duration=10.117542743682861, r='1755924146.8998647 ... 1755924156.9133286'
duration=0.001995563507080078, r='1755924146.8998647 ... 1755924156.9133286'

We can see that the second call to a_slow_func() is served from the cache, which is much faster than the first call, and its result is same as the first call.

Features

  • Built on redis-py, the official Python client for Redis.
  • Simple decorator syntax supporting both async and common functions, asynchronous and synchronous I/O.
  • Support Redis cluster.
  • Multiple caching policies: LRU, FIFO, LFU, RR ...
  • Serialization formats: JSON, Pickle, Dill, MsgPack, YAML, BSON, CBOR, cloudpickle ...
  • Optional handler extension around the four serialization boundaries — async-aware (*_async methods), settable per cache or per function, for patterns like offloading large values to object storage while Redis stores only a small reference.
  • Per-item TTL (Redis ≥ 7.4).
  • Maintenance operations: vacuum to clean expired entries, purge to drop all cache structures — both without blocking Redis.

Installation

  • Install from PyPI:

    pip install redis_func_cache[hiredis]
    
  • Install from source in a editable / development mode:

    git clone https://github.com/tanbro/redis_func_cache.git
    cd redis_func_cache
    pip install --editable --group dev .
    
  • Or install from Github directly:

    pip install git+https://github.com/tanbro/redis_func_cache.git@main
    

The library supports hiredis which is strongly recommended. Installing it can significantly improve performance. It is an optional dependency and can be installed by running: pip install redis_func_cache[hiredis].

If Pygments is installed, the library will automatically remove comments and empty lines from Lua scripts evaluated on the Redis server, which can slightly improve performance. Pygments is also an optional dependency and can be installed by running: pip install redis_func_cache[pygments].

Data structure

The library combines a pair of Redis data structures to manage cache data:

  • The first is a sorted set, which stores the hash values of the decorated function calls along with a score for each item.

    When the cache reaches its maximum size, the score is used to determine which item to evict.

  • The second is a hash map, which stores the hash values of the function calls and their corresponding return values.

This can be visualized as follows:

data_structure

The main idea of the eviction policy is that the cache keys are stored in a set, and the cache values are stored in a hash map. Eviction is performed by removing the lowest-scoring item from the set, and then deleting the corresponding field and value from the hash map.

Here is an example showing how the LRU cache's eviction policy works (maximum size is 3):

eviction_example

The RedisFuncCache executes a decorated function with specified arguments and caches its result. Here's a breakdown of the steps:

  1. Initialize Scripts: Retrieve two Lua script objects for cache hit and update from policy.lua_scripts.
  2. Calculate Keys and Hash: Compute the cache key pair using policy.calc_key_pair, compute the hash value using policy.calc_hash, and compute any additional arguments using policy.calc_ext_args.
  3. Attempt Cache Retrieval: Attempt to retrieve a cached result. If a cache hit occurs, deserialize and return the cached result.
  4. Execute User Function: If no cache hit occurs, execute the decorated function with the provided arguments and keyword arguments.
  5. Serialize Result and Cache: Serialize the result of the user function and store it in Redis.
  6. Return Result: Return the result of the decorated function.
flowchart TD
    A[Start] --> B[Initialize Scripts]
    B --> C{Scripts Valid?}
    C -->|Invalid| D[Raise RuntimeError]
    C -->|Valid| E[Calculate Keys and Hash]
    E --> F[Attempt Cache Retrieval]
    F --> G{Cache Hit?}
    G -->|Yes| H[Deserialize and Return Cached Result]
    G -->|No| I[Execute User Function]
    I --> J[Serialize Result]
    J --> K[Store in Cache]
    K --> L[Return User Function Result]

Concurrency and atomicity

The library guarantees thread safety and concurrency security through the following design principles:

  1. Redis Client Handling

    • The cache does not manage connections itself. It issues commands — single Lua script invocations — through the redis-py client you supply, either directly or via a factory.

    • Client thread safety, event-loop affinity and connection lifecycle are defined by redis-py, not by this library. Consult the redis-py documentation for the client type you use. In short, at the time of writing:

      • The synchronous redis.Redis client issues each command through a thread-safe connection pool, so sharing one client (and its pool) across threads is safe.
      • Pipeline and PubSub objects keep per-object state and must not be shared across threads. This library never creates them, but your own code should be careful.
      • redis.asyncio clients are bound to the event loop that created them. Using one client or pool from a different event loop is undefined behavior — create one pool per event loop.
      • Connections must not be inherited across process forks; rebuild the pool in the child process.
    • Prefer the factory and pool pattern: the factory should return lightweight clients sharing one pre-configured connection pool (e.g. redis.Redis.from_pool(pool)). A factory that creates a brand-new pool per call leaks connections and defeats redis-py's server-side Lua script cache.

    • All cache operations (get, put) are executed via Lua scripts to ensure atomicity, preventing race conditions during concurrent access.

    Here is an example using redis.ConnectionPool to avoid conflicts when the cache accesses Redis:

    import redis
    from redis_func_cache import RedisFuncCache, LruPolicy
    
    redis_pool = redis.ConnectionPool(...)  # Use a pool, not a single client
    factory = lambda: redis.from_pool(redis_pool)  # Use factory, not a static client
    
    cache = RedisFuncCache(__name__, LruPolicy, factory=factory)
    
    @cache
    def your_concurrent_func(...):
        ...
    
  2. Function Execution Concurrency

    Both synchronous and asynchronous functions decorated by RedisFuncCache are executed as-is. Therefore, each function is responsible for its own thread, coroutine or process safety. The only concurrency risk lies in Redis I/O and operations. The cache will use a synchronous Redis client for synchronous functions and an asynchronous Redis client for asynchronous functions. As described above, you should provide an appropriate Redis client or factory to the cache in concurrent scenarios.

  3. Contextual State Isolation

    The ContextVar based mode_context() context manager and other cache control context managers ensure thread and coroutine isolation. Each thread or async task maintains its own independent state, preventing cross-context interference.

Atomicity is a key feature of this library. All cache operations (both read and write) are implemented using Redis Lua scripts, which are executed atomically by the Redis server. This means that each script runs in its entirety without being interrupted by other operations, ensuring data consistency even under high concurrent load.

Each cache policy implements two Lua scripts:

  • A "get" script that attempts to retrieve a value from cache and updates access information
  • A "put" script that adds or updates a value in cache and performs eviction if necessary

These scripts operate on the cache data structures (a sorted set for tracking items and a hash map for storing values) in a single atomic operation. This prevents race conditions that could occur if multiple Redis commands were issued separately.

For Redis Cluster deployments, it's important to note that atomicity is guaranteed only within a single key's hash slot. Since our implementation uses two keys (a sorted set and a hash map) for each cache instance, both keys are designed to belong to the same hash slot. The cluster policies automatically calculate key slots to ensure that all cache data for a single cache instance is always located on the same node in the cluster. This design guarantees that cache operations can be executed atomically within the cluster environment.

These designs enable safe operation in both multi-threaded and asynchronous environments while maintaining high-performance Redis I/O throughput. For best results, use the library with Redis 6.0 or newer to take advantage of native Lua script atomicity and advanced connection management features.

Documentation

More documentation is available in the docs directory (and rendered at Read the Docs):

Getting Started

Prerequisites, a first cached function (sync and async), and how to choose an eviction policy. See docs/usage/quickstart.md for the quick start guide.

Important Considerations

Before using this library, please be aware of the important considerations: the cache stampede risk and its mitigation strategies, plus other key limitations. See docs/considerations.md for details.

Configuration

Cache size & TTL, per-item TTL, serialization, handling non-serializable arguments, multiple key pairs, Redis cluster policies, and cache mode control. See docs/configuration.md for details.

Migration Guide (v0.6 → v0.7)

v0.7 introduced breaking changes to the RedisFuncCache constructor. See docs/migration.md for the summary and migration examples.

Advanced Usage

Custom serializers, the handler extension for the serialization boundaries, custom key formats, and custom hash algorithms (including the make_hasher factory and Scripts subclasses). See docs/advanced-usage.md for details.

Cache Maintenance

Two explicit maintenance operations are available on both RedisFuncCache and its policy (cache.policy.vacuum / cache.policy.purge):

Vacuum: clean expired entries

With a per-item ttl (Redis ≥ 7.4), an expired result disappears from the HASH while its entry in the ZSET lingers as a "ghost" — an eviction slot that points to nothing. vacuum scans the sorted set in batches and removes those members:

removed = cache.vacuum()  # async: removed = await cache.avacuum()
print(f"reclaimed {removed} expired entries")
  • vacuum(batch_size=500) returns the number of ghost entries removed; avacuum() is the async mirror.
  • It never blocks Redis: each batch is one atomic Lua script performing a single ZSCAN step, HEXISTS probes and a ZREM.
  • For details on when ghosts appear and why this design was chosen, see the design note.

Purge: drop cache structures

purge deletes every Redis key the cache owns and returns the number of keys deleted:

n = cache.purge()  # async: await cache.apurge()
print(f"deleted {n} keys")
  • For "multiple" policies — one key pair per decorated function — the keys are enumerated with SCAN (never the blocking KEYS) and deleted in batches of batch_size (default 500) with UNLINK, so a large purge never stalls the server.
  • For "single" policies, the static key pair is deleted with one command.

Known Issues

See docs/considerations.md for the full list of known issues and limitations.

Test

  1. Start a Redis server

  2. Set up REDIS_URL environment variable (Default to redis:// if not defined) to point to the Redis server.

  3. Run the tests:

    uv run --all-extras pytest --cov
    

A Docker Compose file for unit testing is provided in the docker directory to simplify the process. You can run it by executing:

cd docker
docker compose run --rm unittest

It starts the Redis standalone server and cluster automatically (waits until healthy), runs lint, static checks and pytest against Python 3.10–3.14, and propagates the exit code.

The test container uses uv run --frozen, which installs dependencies strictly from uv.lock and never updates it. Note that uv.lock is not tracked in SCM (*.lock is gitignored), so:

  • On a fresh checkout without uv.lock, the test script generates it once automatically before running.

  • If you change dependencies in pyproject.toml, regenerate the lock yourself, otherwise the container keeps testing against the outdated resolution:

    uv lock
    

The container mounts named volumes for the uv download cache and the per-version virtual environments (/venvs), so repeated runs only do incremental installs. To force a full rebuild, remove them with docker compose down -v.

Develop

Clone the project and enter the project directory:

git clone https://github.com/tanbro/redis_func_cache.git
cd redis_func_cache

We can use either the traditional method (venv and pip) of standard library or uv as the environment manager.

  • If using the traditional method, a virtual environment is recommended:

    1. Install a Python development environment on your system. The minimum required Python version is 3.10.

    2. Initialize a virtual environment at sub-directory .venv, then activate it:

      • On Unix-like systems:

        python -m venv .venv
        source .venv/bin/activate
        

        💡 Tip:
        On some older systems, python may be a symbolic link to python2. In such cases, you can use python3 instead.

      • On Windows:

        python -m venv .venv
        .venv\Scripts\Activate
        

        💡 Tip:
        On Windows, the command-line executable for Python may be either python, python3 or py, depending on your installation method.

    3. Install the project with all extras and its development group dependencies:

      pip install -e[all] . --group dev
      
  • If using uv, just the project with all extras and its development group dependencies:

    uv sync --all-groups --dev
    

    A Python virtual environment is created in the .venv directory by uv automatically.

We suggest installing pre-commit hooks:

pre-commit install

ℹ️ Note:
Ensure that you have a stable internet connection during the installation process to avoid interruptions.

Module structure

graph LR
    RedisFuncCache --> Policy
    RedisFuncCache --> Serializer
    RedisFuncCache --> ScriptExecution
    Policy --> Keying
    Policy --> Hasher
    Policy --> Scripts
    Keying --> SingleKeying
    Keying --> MultipleKeying
    SingleKeying --> ClusterSingleKeying
    MultipleKeying --> ClusterMultipleKeying
    Scripts --> LruScripts
    Scripts --> LruTScripts
    Scripts --> FifoScripts
    Scripts --> FifoTScripts
    Scripts --> LfuScripts
    Scripts --> MruScripts
    Scripts --> RrScripts
    LruScripts --> lru_get.lua
    LruScripts --> lru_put.lua
    LruTScripts --> lru_t_get.lua
    LruTScripts --> lru_t_put.lua
    Serializer --> json
    Serializer --> pickle
    Serializer --> dill
    Serializer --> msgpack
    Serializer --> bson
    Serializer --> yaml
    Serializer --> cbor
    Serializer --> cloudpickle
    ScriptExecution --> redis.commands.core.Script
    ScriptExecution --> redis.commands.core.AsyncScript
    RedisFuncCache --> utils.py
    utils.py --> b64digest
    utils.py --> get_callable_bytecode

Class Diagrams

Core class:

classDiagram
    class RedisFuncCache {
        -redis_client: RedisClientTV
        -policy: AbstractPolicy
        -serializer: SerializerPairT
        +__init__(name, policy, redis_client, serializer)
        +__call__(func)
        +decorate(func)
        +exec(user_function, user_args, user_kwds)
        +aexec(user_function, user_args, user_kwds)
    }

    class Policy {
        +keying: Keying
        +hasher: Hasher
        +scripts: Scripts
        +calc_key_pair(f, args, kwds) -> Tuple[str, str]
        +calc_hash(f, args, kwds) -> KeyT
        +purge() -> int
        +apurge() -> int
        +get_size() -> int
        +vacuum() -> int
    }

    class Keying {
        <<interface>>
        key: str
        +calc_key_pair(prefix, name, f) -> Tuple[str, str]
    }

    class Hasher {
        <<interface>>
        __hash_config__: HashConfig
        +calc_hash(f, args, kwds) -> KeyT
    }

    class Scripts {
        <<interface>>
        get_script: str
        put_script: str
        +index_structure: str
    }

    RedisFuncCache --> Policy : uses
    Policy --> Keying
    Policy --> Hasher
    Policy --> Scripts

Composition of the three orthogonal dimensions (keying / hasher / scripts):

classDiagram
    class LruPolicy {
        Policy preset
    }

    class SingleKeying {
        key = "lru"
    }

    class LruScripts {
        get_script = "lru_get.lua"
        put_script = "lru_put.lua"
    }

    class PickleMd5Hasher {
        __hash_config__ = ...
    }

    LruPolicy --> SingleKeying
    LruPolicy --> LruScripts
    LruPolicy --> PickleMd5Hasher

    class FifoPolicy {
        Policy preset
    }

    class FifoScripts {
        get_script = "fifo_get.lua"
        put_script = "fifo_put.lua"
    }

    FifoPolicy --> SingleKeying
    FifoPolicy --> FifoScripts
    FifoPolicy --> PickleMd5Hasher

The four built-in keying variants:

classDiagram
    class Keying {
        <<abstract>>
        key: str
    }

    class SingleKeying
    class MultipleKeying
    class ClusterSingleKeying
    class ClusterMultipleKeying

    Keying <|-- SingleKeying
    Keying <|-- MultipleKeying
    SingleKeying <|-- ClusterSingleKeying
    MultipleKeying <|-- ClusterMultipleKeying

Decorator and proxy:

classDiagram
    class RedisFuncCache {
        +__call__(user_function) -> CallableTV
        +decorate(user_function) -> CallableTV
    }

    class Wrapper {
        +wrapper(*user_args, **user_kwargs)
        +awrapper(*user_args, **user_kwargs)
    }

    RedisFuncCache --> Wrapper

Weak reference:

classDiagram
    class AbstractPolicy {
        -_cache: CallableProxyType[RedisFuncCache]
        +cache: RedisFuncCache
    }

    class RedisFuncCache {
        -_policy_instance: AbstractPolicy
    }

    RedisFuncCache --> AbstractPolicy : creates
    AbstractPolicy --> CallableProxyType : weak reference

Metadata

Release files for redis-func-cache 1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for redis-func-cache 1.0a1
File Size Uploaded
redis_func_cache-1.0a1.tar.gz 153.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for redis-func-cache 1.0a1
File Interpreter ABI Platform
redis_func_cache-1.0a1-py3-none-any.whl Python 3 none any Details

Total release size: 213.1 kB

Release files / redis_func_cache-1.0a1.tar.gz

Download URL redis_func_cache-1.0a1.tar.gz
Size 153.3 kB
Tags Source
SHA-256 checksum
How to use checksums
7b44451866683c217141896b6cebe146631beea73f0e16a866ca32838edb1a0f
BLAKE2b-256 checksum
How to use checksums
7987bae8ebbd2fd07b0b3be46a97d7eb04c9e2b39f75ae73945f383dda9db947
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / redis_func_cache-1.0a1-py3-none-any.whl

Download URL redis_func_cache-1.0a1-py3-none-any.whl
Size 59.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5d4ce74cf43661b241396bbe2453254f2b1d0416c7938082df97edc060cbfd40
BLAKE2b-256 checksum
How to use checksums
b6e19329437d72b1bde6af0f7c5670c950e0cb68d0389a219decd07e6207cd7e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page