Advanced FastAPI Patterns • Lesson 7

Response Caching Patterns

Learn how to implement response caching using ETags and in-memory caches to improve API performance and reduce redundant processing.

🎯 What You'll Learn

  • Understand HTTP caching mechanisms including ETags and conditional requests
  • Implement ETag-based caching with If-None-Match header support
  • Build an in-memory cache with time-to-live (TTL) expiration
  • Apply caching patterns to reduce server load and improve response times

Response Caching Patterns

What You'll Learn

  • Understand HTTP caching fundamentals and why caching matters for APIs
  • Implement ETag-based conditional requests with If-None-Match
  • Build a simple in-memory cache with TTL expiration
  • Distinguish between client-side and server-side caching strategies

Theory

Caching stores the results of expensive operations so they can be reused without recomputation. In APIs, caching reduces server load, decreases response latency, and saves bandwidth.

HTTP Caching Fundamentals

HTTP has built-in caching mechanisms that allow servers and clients to cooperate on reducing unnecessary data transfer.

ETags (Entity Tags)

An ETag is a unique identifier for a specific version of a resource. The server generates an ETag (typically a hash of the response data) and includes it in the response headers:

HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Content-Type: application/json

[{"id": 1, "name": "Laptop"}]

Conditional Requests with If-None-Match

On subsequent requests, the client sends the cached ETag back using the If-None-Match header:

GET /items/ HTTP/1.1
If-None-Match: "a1b2c3d4e5f6"

If the data has not changed, the server responds with 304 Not Modified and no body, saving bandwidth:

HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"

Implementing ETag Caching

To implement ETag support in FastAPI:

  1. Generate the ETag - Hash the response data
  2. Check If-None-Match - Compare with the client's cached ETag
  3. Return 304 or 200 - Skip the body if nothing changed
@app.get("/items/")
async def get_items(request: Request):
    data = items_data
    etag = hashlib.md5(str(data).encode()).hexdigest()

    if_none_match = request.headers.get("if-none-match")
    if if_none_match == etag:
        return JSONResponse(status_code=304, content=None, headers={"ETag": etag})

    return JSONResponse(content=data, headers={"ETag": etag})

In-Memory Caching with TTL

For server-side caching, you can store computed values in memory with an expiration time (Time-To-Live):

cache: dict = {}

@app.get("/cached-time/")
async def get_cached_time():
    cache_key = "current_time"
    cache_ttl = 30  # seconds

    if cache_key in cache:
        cached_value, cached_at = cache[cache_key]
        if time.time() - cached_at < cache_ttl:
            return {"time": cached_value, "cached": True}

    current_time = time.strftime("%Y-%m-%d %H:%M:%S")
    cache[cache_key] = (current_time, time.time())
    return {"time": current_time, "cached": False}

Cache Invalidation

One of the hardest problems in computer science is knowing when cached data is stale:

StrategyDescriptionUse Case
TTL-basedExpire after a fixed time periodData that changes periodically
Event-basedInvalidate when data is updatedWrite-heavy applications
Version-basedUse ETags or version numbersRead-heavy applications
ManualExplicitly clear cache entriesAdmin-triggered updates

Client-Side vs Server-Side Caching

Client-side caching (ETags, Cache-Control headers) reduces network traffic by letting the client decide whether to use cached data.

Server-side caching (in-memory dict, Redis) reduces computation by storing results on the server and serving them directly.

Both strategies can and should be used together for maximum performance benefit.

Cache-Control Headers

While ETags handle validation-based caching, Cache-Control headers control how long responses can be cached:

response.headers["Cache-Control"] = "public, max-age=300"  # Cache for 5 minutes

Common directives:

  • public - Response can be cached by any cache
  • private - Response is specific to one user
  • max-age=N - Cache is valid for N seconds
  • no-cache - Must revalidate before using cached copy
  • no-store - Do not cache at all

Key Concepts

  • ETag - A hash that uniquely identifies a version of a resource
  • If-None-Match - Request header containing the client's cached ETag
  • 304 Not Modified - Response indicating the client's cached version is still current
  • TTL (Time-To-Live) - Duration before a cached entry expires
  • Cache Invalidation - The process of removing or updating stale cache entries
  • JSONResponse - FastAPI's response class for returning JSON with custom headers

Best Practices

  • Use ETags for data that changes infrequently to reduce bandwidth
  • Keep TTL values appropriate for your data freshness requirements
  • Always include an ETag header when implementing conditional requests
  • Use in-memory caching for single-process development; consider Redis for production
  • Include a cached field in responses so clients know if data came from cache
  • Implement cache invalidation whenever the underlying data is modified
  • Be careful with memory usage in in-memory caches; set maximum sizes or clean up periodically
  • Hash the actual data content for ETags rather than using timestamps for accuracy

Additional Resources

💡 Hint

Use hashlib.md5 to generate ETags from your data. Check the If-None-Match request header and return a 304 response if the ETag matches. For the time cache, store a tuple of (value, timestamp) and check if it has expired.

Ready to Practice?

Now that you understand the theory, let's put it into practice with hands-on coding!

Start Interactive Lesson