Cache backends
Choose a cache backend based on where Citry's generated values need to be available. A single-process application can use the default in-memory store. Several workers, hosts, or fragment-serving processes need a shared store.
Citry uses this backend for more than rendered-output caching. The same store can contain generated dependency scripts and optional server-held Events state. Size and protect it for all of those uses.
Choose a backend
| Backend | Visible from | Good fit |
|---|---|---|
InMemoryCache | One process | Local development or one worker |
DiskCache | Workers on one host | Multi-process deployment without a cache service |
RedisCache | Workers on several hosts | Distributed deployment |
DjangoCache | Django's configured cache | Existing Django application |
Citry's DiskCache and Redis adapters wrap clients that you create. Install and configure diskcache or redis in the application that uses the respective adapter. DjangoCache reuses Django's cache framework.
Use the in-process store
Every Citry instance gets a fresh in-process backend unless you pass another one:
from citry import Citry, InMemoryCache
app = Citry()
assert isinstance(app.cache, InMemoryCache)
The store is thread-safe and unbounded by default. Set max_entries to use least-recently-used eviction:
backend = InMemoryCache(max_entries=1_000)
app = Citry(cache=backend)
The limit applies to all values in that backend, not only rendered output. Each Citry() call creates its own store, so one process cannot read values generated by another.
Share values across workers
Use Redis when workers may run on different hosts:
import redis
from citry import Citry
from citry.contrib.caches import RedisCache
client = redis.Redis(host="cache.internal")
backend = RedisCache(client, prefix="myapp:")
app = Citry(cache=backend)
Use DiskCache when the workers share one host and filesystem:
import diskcache
from citry import Citry
from citry.contrib.caches import DiskCache
store = diskcache.Cache("/var/cache/citry")
app = Citry(cache=DiskCache(store))
In a Django application, wrap one of the project's configured caches:
from django.core.cache import caches
from citry import Citry
from citry.contrib.django import DjangoCache
backend = DjangoCache(caches["default"])
app = Citry(cache=backend)
Use the same backend configuration in every process that renders or serves Citry output. This is especially important for HTML fragments, because the browser's asset request may reach a different worker from the render request.
Share render hits safely
A shared store does not automatically make rendered output reusable between Citry instances. Configure both a stable application namespace and a deployment generation:
import os
from citry import Citry
app = Citry(
cache=backend,
extensions_defaults={
"cache": {
"namespace": "storefront-production",
"generation": os.environ["RELEASE_SHA"],
"ttl": 300,
},
},
)
Without both values, render-cache keys remain local to one Citry engine. A namespace on its own still includes the engine identity. A generation without a namespace is invalid.
Use the same namespace and generation in every worker. Change the generation whenever code, templates, extensions, helpers, or configuration can change rendered output. The change makes old entries unreachable immediately; their expiry or the backend's eviction policy removes them later.
Set capacity and artifact limits
Configure the shared store's memory, disk, eviction, and retention in its own client or service. Citry passes TTL values to the adapter, but it does not manage the total capacity of Redis, DiskCache, or Django's backend.
The Cache extension separately limits one rendered artifact to 1,000,000 bytes by default:
app = Citry(
cache=backend,
extensions_defaults={
"cache": {
"max_entry_bytes": 2_000_000,
},
},
)
Set max_entry_bytes to None to remove that configured cap. The artifact format still has an absolute 16 MiB safety limit. An oversized render succeeds but is not stored.
Adapt another string store
The CitryCache protocol has four synchronous methods:
class ApplicationCache:
def get(self, key: str) -> str | None: ...
def set(
self,
key: str,
value: str,
ttl: float | None = None,
) -> None: ...
def delete(self, key: str) -> None: ...
def has(self, key: str) -> bool: ...
Keys and values are strings. get() returns None for an absent or expired entry. set() treats ttl=None as no expiry and receives seconds for a timed entry.
Pass an adapter object or an import string to Citry:
app = Citry(cache=ApplicationCache())
app = Citry(cache="myapp.cache.ApplicationCache")
Citry checks that the four methods exist during construction. Exceptions from backend operations propagate; implement retries or a cache-as-miss policy in the adapter if that is what the application requires.
Clear the right scope
Citry.clear clears an in-process backend and advances the current engine's local cache revision. The built-in shared adapters do not offer store-wide clear() methods, because the underlying store may contain other applications' data.
For a deployment-wide change, move every worker to a new generation. For one rendered-output entry, use the key helpers described in Cache rendered output.
See also
- Cache rendered output for variations, expiry, and privacy requirements.
- HTML fragments for the multi-worker asset delivery case.
- Events state for server-held component state.