Caching Django views and queries with Redis can dramatically speed up your application, but the real magic isn’t in the speedup itself, it’s in how it forces you to think about data freshness and consistency at a fundamental level.

Let’s see it in action. Imagine a Django view that lists all active users. Without caching, every request hits the database.

# views.py
from django.shortcuts import render
from .models import User

def active_users_view(request):
    active_users = User.objects.filter(is_active=True)
    return render(request, 'users/active_users.html', {'users': active_users})

Now, let’s introduce Redis caching using django-redis. First, install it:

pip install django-redis redis

Then, configure settings.py:

# settings.py
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

With this setup, we can cache the view’s output.

# views.py
from django.shortcuts import render
from django.views.decorators.cache import cache_page
from .models import User

@cache_page(60 * 15) # Cache for 15 minutes
def active_users_view_cached(request):
    active_users = User.objects.filter(is_active=True)
    return render(request, 'users/active_users.html', {'users': active_users})

When a user requests this view, Django first checks Redis. If the cached page exists and hasn’t expired, it’s served directly from Redis, bypassing the database and view logic entirely. If not, the view runs, the result is rendered, and then stored in Redis for future requests.

This is view-level caching. But what about the queries within the view? We can cache those too, often using a decorator provided by django-redis or custom logic.

# views.py (with query caching)
from django.shortcuts import render
from django.core.cache import cache # Using the default cache backend
from .models import User

def active_users_view_query_cached(request):
    cache_key = 'active_users_list'
    users = cache.get(cache_key)

    if users is None:
        users = User.objects.filter(is_active=True)
        # Cache for 10 minutes, and mark as a 'user' related item for potential invalidation
        cache.set(cache_key, users, timeout=60 * 10, version=1)
        print("Cache MISS for active users.")
    else:
        print("Cache HIT for active users.")

    return render(request, 'users/active_users.html', {'users': users})

Here, cache.get(cache_key) attempts to retrieve data from Redis. If it’s not there (users is None), the database query runs. The result is then stored in Redis with cache.set(cache_key, users, timeout=600, version=1). The version=1 is a simple way to manage cache invalidation: if you change the query or the data structure, you can increment the version number, and old, invalid data will be ignored.

The core problem caching solves is reducing latency and database load for frequently accessed, relatively static data. It transforms read operations from being I/O bound (database reads) to memory bound (Redis reads), which is orders of magnitude faster.

Internally, django-redis uses the redis-py library. When you call cache.set('my_key', 'my_value', timeout=300), redis-py sends a SET my_key my_value EX 300 command to Redis. When you call cache.get('my_key'), it sends a GET my_key command. Redis itself is a highly optimized in-memory data structure store. It uses a C implementation and efficient data encoding to serve requests in microseconds.

The levers you control are primarily:

  • Cache Keys: These must be unique and descriptive. A common pattern is f'{model_name}:{pk}:{attribute}' or f'{view_name}:{request_params}'.
  • Timeouts: How long data stays fresh. Too short, and you don’t get much benefit. Too long, and users see stale data. This is a trade-off specific to your application’s needs.
  • Cache Invalidation Strategy: This is the hardest part. When data changes (e.g., a user’s is_active status flips), you must remove or update the stale cache entry. This can be done by explicitly calling cache.delete(key) or cache.delete_version(key, version) in your save() methods, signals, or Celery tasks.

A common pitfall is forgetting that cache.set stores a copy of the object. If you retrieve a queryset from the cache, modify an object within that queryset, and then try to save it back without re-fetching from the database or re-caching the modified version, you’re only changing the Python object in memory, not the one in Redis or the database.

The next logical step is to explore more advanced cache invalidation patterns, like using Redis Pub/Sub for real-time invalidation across multiple application instances or implementing cache versioning more systematically.

Want structured learning?

Take the full Django course →