Python Redis Caching: Database Queries and Pages

Caching with Redis is one of the most effective ways to scale Python web applications, drastically reducing database load and network latency. This article covers the essential strategies for caching expensive database query results and web page responses using Redis in Python. You will learn practical implementation approaches, including the Cache-Aside pattern, automated response middleware, serialization techniques, and cache invalidation strategies to maintain data consistency.


1. Strategies for Caching Database Queries

Database operations, particularly complex joins and aggregations, are frequently the primary bottleneck in web applications. Caching query results in Redis stores precomputed datasets in memory for microsecond retrieval.

The Cache-Aside Pattern (Lazy Loading)

The Cache-Aside pattern is the most common strategy for database query caching. The application handles both the data store and the cache:

  1. The application receives a request for data.
  2. It checks Redis for a matching key.
  3. Cache Hit: If present, the application returns the cached data immediately.
  4. Cache Miss: If absent, the application queries the primary database, stores the result in Redis with a defined Time-To-Live (TTL), and then returns the data.
import json
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

def get_user_profile(user_id):
    cache_key = f"user:{user_id}"
    
    # 1. Check Redis
    cached_data = r.get(cache_key)
    if cached_data:
        return json.loads(cached_data)
    
    # 2. Cache Miss: Fetch from Database (mock)
    db_data = fetch_user_from_db(user_id)
    
    # 3. Store in Redis with TTL (e.g., 3600 seconds)
    if db_data:
        r.setex(cache_key, 3600, json.dumps(db_data))
        
    return db_data

Deterministic Key Generation

To cache complex queries or ORM calls, generate deterministic keys based on the query parameters or raw SQL:

Write-Through and Invalidation

Stale data is prevented by invalidating or updating the cache upon data mutation:


2. Strategies for Caching Page Responses

Page caching bypasses template rendering, business logic, and database access entirely by storing complete HTTP responses (HTML or JSON payloads).

Full Page Caching

Full page caching works best on static or semi-static content that is identical for all visitors, such as public blog posts, product detail pages, or landing pages.

from functools import wraps
from flask import request, Response
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

def cache_page(timeout=300):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            cache_key = f"view:{request.full_path}"
            
            # Check for cached response
            cached_page = r.get(cache_key)
            if cached_page:
                return Response(cached_page, content_type="text/html")
            
            # Execute the actual route handler
            response = f(*args, **kwargs)
            
            # Cache the response content
            if response.status_code == 200:
                r.setex(cache_key, timeout, response.get_data())
                
            return response
        return decorated_function
    return decorator

Fragment Caching (Partial Page Caching)

When dynamic, authenticated pages contain both user-specific and static components (e.g., an e-commerce page with a universal product description alongside a personalized shopping cart):

Framework-Level Redis Integration

Most major Python frameworks offer built-in Redis integrations for page caching:


3. Best Practices for Production Caching

  1. Always Set a TTL: Always attach an expiration time (EX or setex) to prevent Redis from running out of memory due to abandoned keys.
  2. Handle Cache Stampedes: When a heavily requested key expires, hundreds of concurrent requests may hit the database at once. Mitigate this by using distributed locks (r.lock()) or probabilistic early expiration algorithms (XFetch).
  3. Configure Eviction Policies: Set maxmemory-policy in Redis (such as allkeys-lru or volatile-lru) to automatically drop the least recently used keys if physical memory limits are reached.
  4. Serialize Efficiently: Use orjson or msgpack instead of standard json or pickle for significantly faster serialization and smaller memory footprints in Python.