FastAPI’s response_cache decorator, powered by aiocache, lets you keep frequently accessed data out of your database and application logic, serving it directly from Redis.

Imagine a product catalog API endpoint. Without caching, every single request for /products hits your database, queries for all products, and then serializes them for JSON. With response_cache, the first request does all that work. Subsequent requests for /products bypass the database and the application logic entirely, fetching the pre-serialized JSON directly from Redis. This dramatically reduces load on your backend and speeds up response times for users.

Let’s see it in action. Here’s a simple FastAPI app with a cached endpoint:

from fastapi import FastAPI
from aiocache import cached
from aiocache.serializers import JsonSerializer
import time

app = FastAPI()

@app.get("/items/{item_id}")
@cached(ttl=60, serializer=JsonSerializer())
async def read_item(item_id: int):
    """
    Simulates fetching an item from a database.
    This function will only execute on cache miss.
    """
    print(f"--- Cache MISS for item {item_id} ---")
    # Simulate a slow database lookup
    time.sleep(2)
    return {"item_id": item_id, "name": f"Item {item_id}", "description": "This is a cached item."}

@app.get("/users")
@cached(ttl=30, key_builder=lambda *args, **kwargs: "all_users", serializer=JsonSerializer())
async def get_users():
    """
    Simulates fetching a list of users.
    The key_builder ensures all requests to this endpoint use the same cache key.
    """
    print("--- Cache MISS for all users ---")
    time.sleep(3)
    return [{"user_id": 1, "name": "Alice"}, {"user_id": 2, "name": "Bob"}]

To run this, you’ll need fastapi, uvicorn, aiocache, and a running Redis instance.

pip install fastapi uvicorn aiocache redis
# Ensure Redis is running (e.g., via Docker: docker run -d -p 6379:6379 redis)
uvicorn main:app --reload

Now, let’s test it.

First request to /items/5:

You’ll see --- Cache MISS for item 5 --- printed in your terminal. The request will take about 2 seconds. The response will be {"item_id": 5, "name": "Item 5", "description": "This is a cached item."}.

Second request to /items/5 (within 60 seconds):

No --- Cache MISS --- message. The response is nearly instantaneous. The data came directly from Redis.

First request to /users:

You’ll see --- Cache MISS for all users --- printed. The request takes about 3 seconds. The response is [{"user_id": 1, "name": "Alice"}, {"user_id": 2, "name": "Bob"}].

Second request to /users (within 30 seconds):

Again, no --- Cache MISS ---. The response is immediate, served from Redis.

The aiocache library is the engine behind the @cached decorator. It handles the connection to Redis, serialization/deserialization of your Python objects into formats Redis can store (like JSON), and managing the Time-To-Live (TTL) for each cached entry.

The @cached decorator takes several key arguments:

  • ttl: The Time-To-Live in seconds. After this duration, the cache entry will be considered stale and a cache miss will occur on the next request.
  • key_builder: A function that generates the unique cache key for a given request. By default, it uses a combination of the function name and its arguments. You can customize this, as shown in get_users where we force a single key "all_users" for all requests to that endpoint.
  • serializer: An object responsible for converting Python data structures to bytes for Redis and back. JsonSerializer is common for FastAPI responses. Other options include PickleSerializer (for arbitrary Python objects, but less secure and cross-language compatible) and StringSerializer.
  • cache: Specifies the cache backend. Defaults to RedisCache. You can explicitly set cache=Cache.REDIS.

The magic of aiocache is how it integrates with FastAPI. When a request hits a route decorated with @cached, aiocache first checks Redis for an entry matching the generated cache key. If found and not expired, it deserializes the data and returns it directly, never even calling your route function. If not found (a cache miss), your route function executes, its return value is serialized and stored in Redis with the given TTL, and then returned to the client.

A common pitfall is not considering how cache keys are generated. For endpoints that should always return the same data for a given set of parameters, the default key builder is usually fine. However, if you have an endpoint that should always return the same data regardless of parameters (like a global configuration setting or a list of all active users), you’ll want to use a key_builder that returns a static string, like key_builder=lambda *args, **kwargs: "my_static_key". This prevents a proliferation of slightly different cache entries for the same logical data.

The serializer choice is also critical. While JsonSerializer is convenient for FastAPI, be aware that it will serialize your data to a JSON string. If your response contains complex types not directly representable in JSON (like datetime objects), you’ll need to handle their serialization and deserialization manually, perhaps by subclassing JsonSerializer or by pre-processing your response data before returning it.

The most surprising thing about caching is how often the cache invalidation problem is harder than the caching problem itself. When data changes in your system (e.g., a product is updated), you need a mechanism to remove or update the corresponding cache entry in Redis. If you don’t, users will continue to see stale data. aiocache provides methods like cache.delete(key) and cache.clear() that you can call from your update logic, but coordinating this across distributed systems is a significant architectural challenge.

The next step is often implementing more sophisticated cache invalidation strategies, or exploring different caching patterns like cache-aside, write-through, or write-behind.

Want structured learning?

Take the full Fastapi course →