2018-08-30 13:01:00+00:00

Two Types of Caching

There are two types of caching: downstream and upstream caching.

With downstream caching, the cache is stored in external systems, whose storage operations are not explicitly commanded by my application code. It happens at the places where HTTP responses are decrypted, such as in Web Browser or SSL termination. When a downstream cache decides to return a response from it, our app server doesn't even know if there is a request. Downstream caching acts based on response header and content, and it is standardized in RFC 7234.

With upstream caching, we are explicitly wiring Django views with a cache storage. The common upstream caching storages are Redis, Memcached, and DBMS.

Enabling Downstream Caching

Django framework already implemented RFC 7234, so we only need to configure it.

Conditionally Return Empty Response Body

In this setup, the view function is still executed. However, if the response content doesn't change, it won't be returned to the Web Browser. Instead, it instructs the Web Browser to reuse the content from its cache by returning status 304. This setup saves resources on bandwidth usage and data transfer processing.

We need to enable the ConditionalGetMiddleware on Django settings:

MIDDLEWARE = [
    ...
    'django.middleware.http.ConditionalGetMiddleware',
    ...
]

Set The Maximum Age of Data in the Cache

We need to enable the UpdateCacheMiddleware on Django settings:

MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',
    ...
]

Based on the current Django implementation, the cache managed by that middleware has 5 minutes timeout. If we need timeout other than that value, we need to set CACHE_MIDDLEWARE_SECONDS on Django settings:

CACHE_MIDDLEWARE_SECONDS = 60 * 60  # 1 hour

We cannot set CACHE_MIDDLEWARE_SECONDS to None in order to make the cache to never timeout. Instead, we will get error if we did that. The workaround is to set CACHE_MIDDLEWARE_SECONDS to a very large integer.

The settings above apply for both downstream and upstream caching.

For downstream caching, it will generate max-age response header.

For upstream caching, it will set timeout value of the cached data on the cache storage.

Don't Let Intermediate Caches to Cache Logged-In User Content

After the user is logged in, places that are allowed to cache the user's content are either the web browser or our servers.

This view annotation forced that only the Web Browser can downstream cache the response:

from django.views.decorators.cache import cache_control

@cache_control(private=True)
def my_view(request):
    ...

Use Values of Headers Pointed by Vary Header as the Part of Cache Key

The official specification: https://tools.ietf.org/html/rfc7234#section-4.1

The example below means each combination of user-agent and cookie will get its own cache value:

from django.views.decorators.vary import vary_on_headers

@vary_on_headers('User-Agent', 'Cookie')
def my_view(request):
    ...

Per-View max-age Response Header

A view can overrides CACHE_MIDDLEWARE_SECONDS like this:

@cache_control(max_age=3600)
def my_view(request):
    ...

Make Sure Nothing, Including Web Browser nor Our App Server, Will Cache the Response

If you have a GET or HEAD view that is totally dynamic that any effort trying to cache it is a waste of resources, then we can totally disable caching on that view:

@cache_control(private=True, no_cache=True)
def my_view(request):
    ...

Enabling Upstream Caching

Per-Site Setup

To cache the entire site we need to update the MIDDLEWARE list to have this order:

MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',
    ...
    'django.middleware.cache.FetchFromCacheMiddleware'
]

MIDDLEWARE list is a decorator pattern implementation. It means that the Response of the last mentioned middleware will be obtained first, and the Response of the first mentioned middleware will be obtained last. The cached Response obtained from FetchFromCacheMiddleware will eventually be the input for UpdateCacheMiddleware. Therefore, we are not allowed to change that ordering of the MIDDLEWARE list.

Per-View Cache

To cache a view for 15 minutes, do this:

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)
def my_view(request):
    ...

Caching View Containing CSRF Token

Per-view cache requires special handling regarding CSRF token. We need to explicitly include Cookie to the Vary header like this:

from django.views.decorators.csrf import csrf_protect

@cache_page(60 * 15)
@csrf_protect
def my_view(request):
    ...

Low Level Cache

If our view is changing app state but it needs to load some rarely changed data, we probably need to use low level functions:

from django.core.cache import cache

def my_view(request):
    # loads rarely changed data from cache:
    the_value = cache.get_or_set('the_key', 'the default value', 100)
    
    # change app state
    ...

References