Redis Caching in Django Rest Framework, Complete Guide

The basic purpose of caching is to increase data retrieval performance by reducing the need to access the underlying slower storage layer.

We will implement Redis caching in Django Rest Framework properly this time.

Project Setup

  • I'm using Docker to start a Redis database server.
docker run -d --name redis-stack-server -p 6379:6379 redis/redis-stack-server:latest
  • Start a Django project and install all required libraries.
pip install django djangorestframework django-redis
mkdir django-redis-example && cd django-redis-example
django-admin startproject config .
python manage.py startapp core
  • Update config/settings.py accordingly for the rest framework and Redis
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",  # For Django REST Framework
    "core",  # For core application
]
# Connect to Redis server for cache backend
# LINK - https://github.com/jazzband/django-redis#configure-as-cache-backend
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://localhost:6379/1",
    }
}
# Set JSON as a default Renderer
REST_FRAMEWORK = {
    "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
}
  • Create a Model and ModelViewSet to perform CRUD operation on this particular model.
# core/models.py
class City(models.Model):
    name = models.CharField(max_length=100)
    state_code = models.CharField(max_length=100)
    state_name = models.CharField(max_length=100)
    country_code = models.CharField(max_length=100)
    country_name = models.CharField(max_length=100)

    def __str__(self):
        return "%s: %s: %s" % (self.name, self.state_code, self.country_code)
# core/serializers.py
from core.models import City

class CitySerializer(serializers.ModelSerializer):
    class Meta:
        model = City
        fields = "__all__"
# core/views.py
from rest_framework.viewsets import ModelViewSet

from core.models import City
from core.serializers import CitySerializer

class CityView(ModelViewSet):
    serializer_class = CitySerializer
    queryset = City.objects.all()

    http_method_names = ["get", "post", "patch", "delete"]
# config/urls.py
from django.urls import path, include

from rest_framework.routers import DefaultRouter
from core.views import CityView

router = DefaultRouter()
router.register("", CityView, basename="root")

urlpatterns = [
    path("", include(router.urls)),
]

Here the basic setup is complete and we can List, Update, Retrieve and Delete from the City model at localhost:8000.


Adding Caching in the view

Now we will modify ModelViewSet's list() function to cache the GET request data for 5 minutes.

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

class CityView(ModelViewSet):
    ...
    @method_decorator(cache_page(300, key_prefix="city-view"))
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

When we call the GET request for the first time, the response will be stored in our Redis database with two keys.

:1:views.decorators.cache.cache_header.city-view.cb4fbe6a5f6caec1f1715b85fecd2d7f.en-us.UTC
:1:views.decorators.cache.cache_page.city-view.GET.cb4fbe6a5f6caec1f1715b85fecd2d7f.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC

Here, 1 is the database name, we are using Django's default cache_page therefore the prefix views.decorators.cache is always going to be the same for Redis, Memcached, or any other caching database. city-view is the key_prefix we assigned in cache_page in our views file. We should always use unique keys. In the end .en-us.UTC is our project's LANGUAGE_CODE and TIME_ZONE which are already specified in the settings.py file.


Problem: Suppose we call the GET request and the response is cached for 5 minutes, now we updated something in the city model. We are not able to retrieve that updated data until the cached data is expired. Therefore we need to make sure whenever the data is updated in our database, the cache related to that model is also updated or deleted.

Solution: we will create a function that will delete the cached data by passing the key_prefix we specified in the view which iscity-view in our case.

# config/utils.py
from django.core.cache import cache
from django.conf import settings

# LINK - https://github.com/jazzband/django-redis#scan--delete-keys-in-bulk
def delete_cache(key_prefix: str):
    """
    Delete all cache keys with the given prefix.
    """
    keys_pattern = f"views.decorators.cache.cache_*.{key_prefix}.*.{settings.LANGUAGE_CODE}.{settings.TIME_ZONE}"
    cache.delete_pattern(keys_pattern)

to use this function in our view we will override create, destroy, and partial_update functions a little bit.

from config.utils import delete_cache

class CityView(ModelViewSet):
    ...
    CACHE_KEY_PREFIX = "city-view"

    @method_decorator(cache_page(300, key_prefix=CACHE_KEY_PREFIX))
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

    def create(self, request, *args, **kwargs):
        response = super().create(request, *args, **kwargs)
        delete_cache(self.CACHE_KEY_PREFIX)
        return response

    def destroy(self, request, *args, **kwargs):
        response = super().destroy(request, *args, **kwargs)
        delete_cache(self.CACHE_KEY_PREFIX)
        return response

    def partial_update(self, request, *args, **kwargs):
        response = super().partial_update(request, *args, **kwargs)
        delete_cache(self.CACHE_KEY_PREFIX)
        return response

The reason we are using a variable response to assign the super function instead of directly returning that is that if any validation error or object not found error is thrown. The cache will not be deleted. The proper update needs to be done before deleting it.

Click here to check out the source code.