Theme
Version
GitHub PyPI Discord
On this page

Cache backends

With several worker processes or hosts, a page rendered by one worker can ask another worker for a generated file and get a 404, and cached output is never shared between workers.

Both happen because Citry keeps cached HTML and some generated files in a cache backend, the store that holds those values, and by default each Citry instance keeps its own store in memory. That is enough for one process. For several workers, give them all the same shared backend.

The backend holds more than cached rendered output:

  • the generated scripts that load each page's JavaScript and CSS;
  • Events State that the server keeps for the browser, when you turn that on;
  • the compiled code and styles of interactive components, when you pass a backend.

Size and protect it for all of these uses.

Choose a backend

BackendShared betweenGood fit
InMemoryCacheNothing; one process onlyLocal development or one worker
DiskCacheWorkers on one hostSeveral processes, no cache service
RedisCacheWorkers on several hostsA deployment on several hosts
DjangoCacheWhatever Django's cache sharesAn existing Django application

Citry's DiskCache and Redis classes wrap a client you create, so install diskcache or redis in your application. DjangoCache uses Django's own cache framework.

InMemoryCache limits

Every Citry instance gets its own in-memory store unless you pass another backend:

from citry import Citry, InMemoryCache

app = Citry()
assert isinstance(app.cache, InMemoryCache)

The store is safe to use from several threads and has no size limit by default. Set max_entries to cap it. When the store is full, it drops the entry that was used longest ago:

backend = InMemoryCache(max_entries=1_000)
app = Citry(cache=backend)

max_entries must be a positive int or None; any other value raises ValueError. The limit counts every value in the store, not only rendered output.

Share a store

Use Redis when workers 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 file system:

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)

Configure the same backend in every process that renders or serves Citry output. This matters most for HTML fragments, where the browser's request for a fragment's files may reach a different worker from the one that rendered it.

Share cached output

A shared store alone does not let workers reuse each other's cached output. Each Citry instance keeps its rendered-output entries separate until you give the app two values:

  • namespace: a name for your application and environment, so apps that share one store never read each other's entries;
  • generation: a value you change on every deploy, such as the release commit, so new code never reuses HTML rendered by old code.
import os

from citry import Citry

app = Citry(
    cache=backend,
    extensions_defaults={
        "cache": {
            "namespace": "storefront-production",
            "generation": os.environ["RELEASE_SHA"],
            "ttl": 300,
        },
    },
)

Use the same namespace and generation in every worker. Change the generation whenever code, templates, extensions, helpers, or configuration can change rendered output. Old entries become unreachable at once and leave the store when they expire or the store drops them.

Both values are needed. A namespace alone still keeps entries separate for each Citry instance. A generation without a namespace, or an empty string for either, raises ValueError when you create Citry.

Limit asset memory

An interactive page loads its compiled component code and styles from URLs after the HTML arrives, and a page that stays open may request one much later. Without a configured backend, each Citry instance keeps these files in its own process, up to 64 MiB by default. Past the limit, it drops the files used longest ago, but never the files of a page it is still rendering.

If the limit is too small, a page that has been open for a while stops loading a component, and the browser shows a 404 for a URL containing /ext/events/definitions/ or /ext/events/assets/. Raise the limit, or pass None to remove it:

app = Citry(vue_asset_max_bytes=256 * 1024 * 1024)
app = Citry(vue_asset_max_bytes=None)

The value must be a positive int or None. Another type raises TypeError, and zero or a negative number raises ValueError.

When you pass a backend, Citry stores these files there without an expiry, and vue_asset_max_bytes has no effect. Give the backend enough room that it does not drop them while pages are still open.

Limit render size

Citry does not store a single rendered result larger than 1,000,000 bytes by default. Change the limit with max_entry_bytes:

app = Citry(
    cache=backend,
    extensions_defaults={
        "cache": {
            "max_entry_bytes": 2_000_000,
        },
    },
)

None removes this limit, but a stored render can never exceed 16 MiB. Any value other than a positive int or None raises ValueError when you create Citry. A render over the limit still appears on the page; it is just not stored.

Set the total memory, disk space, and eviction policy of a shared store in Redis, DiskCache, or Django itself. Citry passes each entry's expiry to the store, but does not manage the store's total size.

Clear cached values

Citry.clear empties an in-memory backend and stops this Citry instance from finding its existing rendered-output entries. The built-in shared adapters have no clear() method, because the store may hold other applications' data.

To invalidate every worker after a deploy, change the generation. To remove a single rendered entry, build its key as shown in Cache rendered output.

CitryCache adapters

Any object with these four synchronous methods works as a backend; the CitryCache protocol describes them:

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 a missing or expired entry. set() receives the expiry in seconds, or None for no expiry.

Pass the adapter object, or an import string, to Citry:

app = Citry(cache=ApplicationCache())
app = Citry(cache="myapp.cache.ApplicationCache")

Citry checks for the four methods when you create Citry and raises TypeError if one is missing. Errors raised by these methods reach your code; add retries, or treat errors as misses, inside the adapter if your application needs that.

Edge cases

Temporary 404s

Each process checks whether a shared backend still holds a file at most once a minute, not on every render. If the backend drops a file, the next render after that minute writes it back. Until then, other workers answer 404 for it.