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 "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)
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.