Skip to content

Backends

Where entries and state live: in memory, Redis, Valkey, GLIDE, memcached, DynamoDB, two levels. In fastapi_alcazaba.backends.

In the guide: Backends, Two-level cache.

fastapi_alcazaba.backends.Backend

Bases: ABC

get_with_ttl abstractmethod async

get_with_ttl(key: str) -> tuple[int, bytes | None]

(seconds left, value); -1 seconds when the entry doesn't expire or the backend can't tell, and (0, None) when there is no entry.

clear abstractmethod async

clear(namespace: str | None = None, key: str | None = None) -> int

Delete every entry of namespace (keys starting with ":"), or else the entry key; returns how many were deleted.

acquire_lock async

acquire_lock(key: str, token: str, ttl: float) -> bool

Take the lock key for ttl seconds unless someone holds it; True if taken.

release_lock async

release_lock(key: str, token: str) -> None

Release the lock key if it's still held with token (it may have expired).

extend_lock async

extend_lock(key: str, token: str, ttl: float) -> bool

Make the lock key expire ttl seconds from now if it's still held with token; False when it isn't (it expired, maybe taken by someone else).

add_tags async

add_tags(key: str, tags: Sequence[str], expire: int | None) -> None

Record that the entry key belongs to each of tags (already namespaced keys).

invalidate_tags async

invalidate_tags(tags: Sequence[str]) -> int

Delete every entry recorded under tags; returns how many were deleted.

invalidate_tags_with_keys async

invalidate_tags_with_keys(tags: Sequence[str]) -> tuple[int, list[str] | None]

invalidate_tags, also returning the keys of the entries recorded under tags, or None when the backend can't tell (TieredBackend tells the other processes to drop their copies of them).

incr async

incr(key: str, amount: int = 1, expire: int | None = None) -> int

Add amount to the counter key atomically and return its new value. A missing counter starts at 0 and, with expire, is deleted that many seconds after it's created.

get_counter async

get_counter(key: str) -> int

The value of the counter key; 0 when there is none.

incr_window async

incr_window(key: str, previous_key: str, *, amount: int, limit: int, weight: float, expire: int) -> tuple[bool, int, int]

The rate limiter's sliding window: add amount to the counter key only if previous * weight + current + amount <= limit, where current is that counter and previous the counter previous_key. Returns whether it was added and both counts (current with the addition, if any).

Backends that can, check and add atomically. This default adds and then takes the amount back when it didn't fit, so concurrent requests may briefly see it.

fastapi_alcazaba.backends.inmemory.InMemoryBackend

InMemoryBackend(max_size: int | None = 10000)

Bases: Backend

Cache in the memory of the process: for development, tests and single-process apps.

Each instance has its own store, bounded to max_size entries (the least recently used are evicted): a store that never evicted would grow with every new query string until the process ran out of memory.

State (rate-limit counters, revoked tokens, one-time codes, sessions, idempotency records: Cache.state_key) is never evicted while it lives: forgetting a revocation would accept the token again. Each kind of state has a store of its own, bounded to max_size live entries; a write to a full one raises BackendUnavailable, like Redis with noeviction, and each tool fails closed. So a flood of idempotency keys fills the idempotency records, never the revocations or the sessions.

incr_window async

incr_window(key: str, previous_key: str, *, amount: int, limit: int, weight: float, expire: int) -> tuple[bool, int, int]

Atomic: nothing awaits between the check and the addition.

fastapi_alcazaba.backends.redis.RedisBackend

RedisBackend(redis: Redis | RedisCluster, *, scan_count: int = 500)

Bases: RedisLikeBackend

Redis, or any server speaking its protocol (Valkey, KeyDB, Dragonfly...), through redis-py.

Create the client without decode_responses=True: cached values are bytes.

fastapi_alcazaba.backends.valkey.ValkeyBackend

ValkeyBackend(valkey: Valkey | ValkeyCluster, *, scan_count: int = 500)

Bases: RedisLikeBackend

Valkey through its own client, valkey-py. A redis-py client pointed at Valkey works too, with RedisBackend.

Create the client without decode_responses=True: cached values are bytes.

fastapi_alcazaba.backends.glide.GlideBackend

GlideBackend(client: GlideClient | GlideClusterClient, *, scan_count: int = 500)

Bases: Backend

Valkey (or Redis) through Valkey GLIDE, the official client with a Rust core: GlideClient or GlideClusterClient, created with await GlideClient.create(config).

Same behaviour as RedisBackend: SCAN + UNLINK to clear a namespace, distributed locks, tags and counters.

fastapi_alcazaba.backends.memcached.MemcachedBackend

MemcachedBackend(mcache: Client)

Bases: Backend

incr async

incr(key: str, amount: int = 1, expire: int | None = None) -> int

memcached counters don't go below 0.

fastapi_alcazaba.backends.dynamodb.DynamoBackend

DynamoBackend(table_name: str, region: str | None = None, *, endpoint_url: str | None = None)

Bases: Backend

Amazon DynamoDB backend provider

This backend requires an existing table within your AWS environment to be passed during backend init. If ttl is going to be used, this needs to be manually enabled on the table using the ttl key. Dynamo will take care of deleting outdated objects, but this is not instant so don't be alarmed when they linger around for a bit.

As with all AWS clients, credentials will be taken from the environment. Check the AWS SDK for more information.

Usage: >> dynamodb = DynamoBackend(table_name="your-cache", region="eu-west-1") >> await dynamodb.init() >> FastAPICache.init(dynamodb)

endpoint_url points to DynamoDB Local, LocalStack or a VPC endpoint.

incr async

incr(key: str, amount: int = 1, expire: int | None = None) -> int

Counters live in the count attribute. An expired counter that DynamoDB hasn't deleted yet goes on counting (the rate limiter never reuses one: its keys change every window).

incr_window async

incr_window(key: str, previous_key: str, *, amount: int, limit: int, weight: float, expire: int) -> tuple[bool, int, int]

Atomic for the current window: a conditional update. The previous window is read first; it no longer changes (but for clients with a clock behind).

fastapi_alcazaba.backends.tiered.TieredBackend

TieredBackend(remote: Backend, *, local_ttl: int = 5, local_max_size: int = 1000, invalidation_channel: str | None = 'fastapi_alcazaba:invalidations')

Bases: Backend

Two levels: a small cache in the memory of each process in front of a shared backend.

A hit in the process skips the network. Each local copy lives at most local_ttl seconds and never longer than the shared entry, which bounds how stale it can get when another process changes the value.

With a Redis or Valkey shared backend whose client supports pub/sub (RedisBackend, ValkeyBackend; not a valkey-py cluster), writes, clears and tag invalidations are announced on invalidation_channel, and every process drops its copy right away (a tag invalidation names the keys it deleted; beyond 1000, every process drops all its copies). Local copies are only kept while the process is subscribed: when the subscription drops (and messages may have been missed), the local level is emptied and bypassed until it's back. A value read from the shared backend while an invalidation arrives isn't kept: it may be the old one.

Values are kept locally as the shared backend stores them, so signed or encrypted entries stay signed or encrypted in memory too.

close async

close() -> None

Stop listening for invalidations (the next call starts again) and drop the copies.