Theme
Version
GitHub PyPI Discord
On this page

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

BackendVisible fromGood fit
InMemoryCacheOne processLocal development or one worker
DiskCacheWorkers on one hostMulti-process deployment without a cache service
RedisCacheWorkers on several hostsDistributed deployment
DjangoCacheDjango's configured cacheExisting 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