Skip to content

Redis Cache

The Redis cache adapter provides persistent, distributed caching with TTL support. It is the recommended cache provider for production environments.

Overview

The Redis cache is designed for:

  • Production environments requiring persistent cache storage
  • Multi-process deployments where cache must be shared across workers
  • TTL-based expiry with millisecond precision
  • Pattern-based operations for bulk retrieval and cleanup

Installation

pip install "protean[redis]"

# Or install the Redis package separately
pip install "redis>=8.0.0,<8.2.0"

Configuration

[caches.default]
provider = "redis"
URI = "redis://localhost:6379/2"
TTL = 300

Configuration Options

Option Default Description
provider Required Must be "redis" for the Redis cache
URI Required Redis connection string
TTL 300 Default time-to-live in seconds. See below.

What counts as a TTL

A TTL must be a positive, finite number of seconds. It may also be a string holding one, so an environment variable works:

[caches.default]
provider = "redis"
TTL = "${CACHE_TTL|3600}"

Substitution runs over already-parsed TOML strings, so a TTL sourced from the environment arrives as a string; that is why the string form is accepted rather than merely tolerated.

Anything else, including 0, a negative, nan or inf, raises a ConfigurationError naming the cache.

The same rule applies wherever a TTL is passed: cache.add(projection, ttl=...) and cache.set_ttl(key, ttl) take the same shapes and reject the same ones. Omitting the TTL (or passing an empty string) uses the cache's configured TTL.

Connection String Format

redis://[[username]:[password]@]host[:port][/database]

# Examples:
redis://localhost:6379/2           # Local Redis, database 2
redis://:password@redis.prod:6379  # With password

Tip

Use a different Redis database number (the /2 suffix) for the cache than for the broker to keep concerns separated.

Usage

Projections are automatically stored in the cache when a projector writes to a cache-backed projection. You can also interact with the cache directly. This example needs a running Redis server:

# fragment
import os

from protean import Domain
from protean.fields import Float, Identifier

domain = Domain(name="Orders")
domain.config["caches"]["default"] = {
    "provider": "redis",
    "URI": os.environ.get("REDIS_URL", "redis://localhost:6379/2"),
    "TTL": 300,
}


@domain.projection(cache="default")
class OrderSummary:
    order_id: Identifier(identifier=True)
    total: Float()


domain.init(traverse=False)
with domain.domain_context():
    # Get the cache that holds the projection
    cache = domain.cache_for(OrderSummary)

    # Check connectivity
    reachable = cache.ping()  # True if Redis is reachable

    # Store a projection; its key is "order_summary:::ord-123"
    cache.add(OrderSummary(order_id="ord-123", total=42.5))

    # Retrieve a cached projection
    entry = cache.get("order_summary:::ord-123")

    # Count cached entries
    count = cache.count("order_summary:::*")

    # Set a custom TTL on a specific key
    cache.set_ttl("order_summary:::ord-123", ttl=600)  # 10 minutes
    remaining = cache.get_ttl("order_summary:::ord-123")

    # Remove all entries
    cache.flush_all()

domain.cache_for(OrderSummary) returns the cache that holds the projection. Each entry's key is the projection's name in snake case, then :::, then its identifier. get_ttl returns the seconds left as a float, so right after set_ttl(..., ttl=600) it returns a value just under or equal to 600.0.

Limitations

  • Requires Redis Server: Redis must be installed and running. Use make up to start Protean's Docker-based development services.
  • Memory Bound: Redis stores data in memory. Ensure sufficient RAM for your cache working set.
  • No Complex Queries: The cache API supports key-based and pattern-based lookups only. For complex queries, use a database-backed projection instead.