Skip to main content

Simple and lightweight library for caching responses with minimal external dependencies.

Project description

# fastapi-cachemate

[![CI](https://github.com/diyelise/fastapi-cachemate/actions/workflows/ci.yml/badge.svg)](https://github.com/diyelise/fastapi-cachemate/actions/workflows/ci.yml)
[![codecov](https://codecov.io/github/diyelise/fastapi-cachemate/branch/master/graph/badge.svg)](https://codecov.io/github/diyelise/fastapi-cachemate)
![Python](https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13-blue)

Simple and lightweight library for caching responses with minimal external dependencies.

Inspired by [fastapi-cache](https://github.com/long2ice/fastapi-cache).

## Features

- Fully async
- Easy integration with FastAPI
- Fast serialization and deserialization
- Smart compression and decompression for storage
- Cache stampede mitigation
- Bypass mode for skipping cache
- Support filters from Pydantic, dataclasses, and custom classes

## Requirements

- FastAPI
- redis-py

## Install

> pip install fastapi-cachemate

OR

> poetry add fastapi-cachemate

## Quick Start

fastapi-cachemate requires a small setup. The minimal example below prepares the cache layer.

```python
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

import redis
from fastapi import FastAPI
from pydantic_settings import SettingsConfigDict

from fastapi_cachemate import BaseCacheSettings, CacheSetup
from fastapi_cachemate.core.backends.redis import RedisBackend
from fastapi_cachemate.core.locks.redis import RedisLockManager


class ApiCacheSettings(BaseCacheSettings):
host: str = "127.0.0.1"
port: int = 6379
db: int = 0
ttl_lock_seconds: int = 5
buffer: float = 0.2

# See BaseCacheSettings for more options.
model_config = SettingsConfigDict(env_prefix="api_cache_")


api_cache_settings = ApiCacheSettings()


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
_backend = redis.asyncio.Redis(
host=api_cache_settings.host,
port=api_cache_settings.port,
db=api_cache_settings.db,
)
redis_backend = RedisBackend(client=_backend)
try:
CacheSetup.setup(
backend=redis_backend,
lock_manager=RedisLockManager(backend=redis_backend),
settings=api_cache_settings,
)
yield
finally:
await CacheSetup.close()
await redis_backend.close()
await _backend.connection_pool.disconnect()


app = FastAPI(lifespan=lifespan)
```

Now create an endpoint and cache it for 300 seconds:

```python
from fastapi_cachemate.cache import cache_response


@app.get("/blogs/{blog_id}")
@cache_response(ttl=300)
async def get_blog_by_id(blog_id: BlogId) -> dict[str, BlogId]:
return {"blog_id": blog_id}
```

After a second request you should see headers like:

```text
cache-control: max-age=300
content-length: 14
content-type: application/json
date: Thu, 26 Feb 2026 15:35:19 GMT
server: uvicorn
x-cache-status: HIT
```

`X-Cache-Status` can be one of `HIT`, `MISS`, `BYPASS`, or `NO_CACHE`.

## Support any filter variants

You can use filter objects based on Pydantic or dataclasses, or your own classes.

```python
from typing import Any, Annotated

from fastapi import Depends
from pydantic import BaseModel, ConfigDict, Field


class BlogPydanticFilter(BaseModel):
blog_id: str | None = Field(None, description="blog id", alias="id")
slug: str | None = Field(None, description="blog slug")
page: int = Field(1, ge=1)
max_page: int = Field(10, ge=1, le=100)

model_config = ConfigDict(populate_by_name=True)


@app.get("/blogs")
@cache_response(ttl=300)
async def get_blogs(
db_session: AsyncSession = Depends(get_db_session),
filter_: Annotated[BlogPydanticFilter, Query()],
) -> dict[str, Any]:
...
```
It will be unpacked like this:

- id
- slug
- page
- max_page

`db_session` will be passed as a dependency, because it's not a filter.

More filter examples are in [examples/app.py](examples/app.py).

## Smart storage

fastapi-cachemate tries to be memory friendly and applies compression for large objects.
It supports three compression modes:

- all: compress every value
- smart: compress based on an internal heuristic (default 1 KB)
- disabled: disable compression for all values

Large values (for example, over 20 KB) can increase CPU usage and may require monitoring.

## Cache stampede mitigation

When a popular key is close to expiring, many clients can trigger recomputation at once.
fastapi-cachemate mitigates this with an early refresh window: while a key is still valid, it checks
whether the key is within a refresh threshold based on the last compute time and buffer.
If so, a single request acquires a lock and refreshes the cache in the background while
other requests keep using the cached value.

## Bypass mode

There is no explicit cache invalidation yet, but you can skip cache in two ways:

- Client controlled: send `Cache-Control: no-cache` or `no-store`. Cache is skipped and
`X-Cache-Status` is `NO_CACHE`.
- Server controlled: send the configured bypass header. Cache is skipped and
`X-Cache-Status` is `BYPASS`.

For example, define your values in the settings or through the work environment.
```python
class ApiCacheSettings(BaseCacheSettings):
host: str = "127.0.0.1"
port: int = 6379
db: int = 0
ttl_lock_seconds: int = 5
buffer: float = 0.2
bypass_header: str = "X-CACHE-BYPASS"
bypass_value: str = "test"
...
```

If you need per-request control inside the app, use the context flag:

```python
from collections.abc import AsyncGenerator

from fastapi import Depends

from fastapi_cachemate.cache import is_bypass


async def bypass_authorized_request(
user: User | None = Depends(maybe_get_user),
) -> AsyncGenerator[None, None]:
"""
Tell the cache layer to skip cached responses for authorized users.
"""
if user:
token = is_bypass.set(True)
try:
yield
finally:
is_bypass.reset(token)
else:
yield


@app.get(
"/blogs/{blog_id}",
summary="Get blog by id",
dependencies=[Depends(bypass_authorized_request)],
)
@cache_response(ttl=300)
async def get_blog_by_id():
...
```

## Tests and coverage

Use a Makefile to run tests and coverage generate reports

## License

This project is licensed under the Apache-2.0 License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_cachemate-0.1.1.tar.gz (16.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_cachemate-0.1.1-py3-none-any.whl (19.7 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_cachemate-0.1.1.tar.gz.

File metadata

  • Download URL: fastapi_cachemate-0.1.1.tar.gz
  • Upload date:
  • Size: 16.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for fastapi_cachemate-0.1.1.tar.gz
Algorithm Hash digest
SHA256 8bbee7ae83b6f1d71249bf98bad85e04470b64273739d32a82547b1ec51ac844
MD5 38db1d1efc656ecc107bd20f0f415468
BLAKE2b-256 6bea5e4c056e85611fe0c355947d54e5e2a3dadfe3f541bae5ff42f96bf36487

See more details on using hashes here.

File details

Details for the file fastapi_cachemate-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for fastapi_cachemate-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e0d2fc58172f75bcf2deea696284e83f4b0b4a2e589b0fb8c2cccfcddff2683a
MD5 9d42f187c75456783fba344eab3be309
BLAKE2b-256 3c17a4ad6c636353009bc02ec0ded972baa975d0fa0b7d5536b1ff0e02202769

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page