Skip to content

cache_response

@cache_response caches the rendered HTTP output of a view method or function-based view. Use it when the whole response payload is safe to cache as raw bytes, the view body and serializer work is the slow part, and the view does not need per-call side effects.

@cache_result caches the return value of a function. @cache_response caches the rendered HTTP response (content, status code, headers). On a hit it rebuilds a plain HttpResponse and skips the view body, the serializer, and the renderer entirely.

Basic usage

from rest_framework.response import Response
from rest_framework.views import APIView
from restflow.caching import cache_response


class TimelineView(APIView):
    @cache_response(ttl=60)
    def get(self, request):
        return Response({"items": expensive_lookup()})

The default key constructor builds a key from the request's query parameters and the view's URL kwargs. Subsequent requests with the same query string and URL kwargs return the cached response without running get again.

Parameters

Parameter Type Default Effect
key_constructor KeyConstructor subclass, instance, or dict of fields ResponseCacheKeyConstructor How cache keys are built.
ttl int \| None 3600 Time-to-live in seconds. None means no expiration.
invalidates_on list[InvalidationRule] None Rules that fire on Django model signals to invalidate the cache.
cache_if Callable None Predicate on the response. The response is cached only when this returns truthy.
cache_unless Callable None Predicate on the response. The response is skipped (not cached) when this returns truthy.
set_cache_headers bool False When True, attach X-Cached-at, X-Cache-reset-at, and X-Cache-status headers to every returned response.

cache_if and cache_unless are mutually exclusive on a single decorator.

Default key constructor

ResponseCacheKeyConstructor ships with two fields:

class ResponseCacheKeyConstructor(KeyConstructor):
    query_params = QueryParamsKeyField("*", hash_value=True)
    path_params = ViewKwargsKeyField("*", partition=True)

query_params hashes the full query string. path_params captures the view method's URL kwargs (everything except self and request) and marks them as the partition so a single instance's cache can be wiped as a group.

Subclass to add a user partition, narrow the captured fields, or layer extra fields on top.

from restflow.caching import (
    ResponseCacheKeyConstructor, ArgsKeyField, RequestValueKeyField,
)


class UserTimelineKey(ResponseCacheKeyConstructor):
    user = RequestValueKeyField("user.id", partition=True)

    class Meta:
        version = 1
        namespace = "UserTimeline"

Function-based views

@cache_response works on DRF's @api_view decorator. Apply @cache_response closest to the function so it wraps the original callable before @api_view turns it into a class-based view.

from rest_framework.decorators import api_view
from rest_framework.response import Response
from restflow.caching import cache_response


@api_view(["GET"])
@cache_response(ttl=60)
def timeline(request):
    return Response({"items": expensive_lookup()})

The view's accepted_renderer is read from the DRF parser context, so the cached response renders the same way as a normal @api_view call.

Sync-only function-based views

DRF's @api_view decorator dispatches synchronously. Async functions wrapped in @api_view return coroutines that DRF cannot await. For async function-based caching, use restflow's AsyncAPIView and put @cache_response on the method.

Async views

@cache_response detects an async view method at decoration time and routes every cache I/O through Django's async cache API.

from restflow.responses import Response
from restflow.views import AsyncAPIView
from restflow.caching import cache_response


class UserMeView(AsyncAPIView):
    @cache_response(ttl=60)
    async def get(self, request):
        return Response(await fetch_user_payload(request.user.id))

When the wrapped method returns a restflow.responses.Response, restflow renders it through arender so the cache write stays on the event loop. DRF's Response falls back to sync render.

Invalidating on model changes

Pair @cache_response with InvalidationRule the same way as @cache_result. When the model signal fires, the rule maps fields from the saved instance into the function's kwargs and wipes the matching cache entries.

from restflow.caching import (
    KeyConstructor, ArgsKeyField, cache_response, InvalidationRule,
)


class UserKey(KeyConstructor):
    pk = ArgsKeyField("pk", partition=True)

    class Meta:
        namespace = "UserDetail"


class UserView(APIView):
    @cache_response(
        key_constructor=UserKey,
        ttl=300,
        invalidates_on=[
            InvalidationRule(
                model=User,
                field_mapping={"pk": "pk"},
                watch_fields=["username", "email"],
            ),
        ],
    )
    def get(self, request, pk=None):
        user = User.objects.get(pk=pk)
        return Response(UserSerializer(user).data)

A key constructor used for invalidation should rely only on values reachable from the model instance through field_mapping. The signal handler never has the original request, so QueryParamsKeyField and RequestValueKeyField resolve to empty during invalidation. For partition wipes (delete_by_prefix), put the model-derived fields in the partition and they will match every cached variant.

Rewarm is not supported for view-method caches

InvalidationRule(rewarm=True) recalls the wrapped function with the kwargs built from field_mapping. View methods have a (self, request, ...) signature that the signal handler cannot reconstruct, so rewarming a @cache_response-decorated method will always fail to bind and fall back to deletion. Stick to the default rewarm=False and let the next request rebuild the response. See Invalidation Rules > Refresh instead of delete for the same constraint applied to plain @cache_result.

Conditional caching

cache_if and cache_unless evaluate against the rendered response. Skip caching for errors or empty payloads.

class V(APIView):
    @cache_response(
        ttl=60,
        cache_if=lambda response: response.status_code < 400,
    )
    def get(self, request):
        return Response({"ok": True})

For async views, both predicates may be async def.

Surfacing cache status to the client

Pass set_cache_headers=True to attach the cache metadata as response headers on every call. Clients and monitoring can then tell hits from misses without a separate lookup.

class V(APIView):
    @cache_response(ttl=60, set_cache_headers=True)
    def get(self, request):
        return Response({"v": 1})
Header Value
X-Cache-status HIT, MISS, STALE, BYPASS, or REFRESH.
X-Cached-at ISO timestamp recorded when the value was written.
X-Cache-reset-at ISO timestamp when the entry will expire, when a TTL is set.

Wrapper methods

The decorated method becomes a CachedResponseWrapper. It inherits every method on CachedWrapper, so the management surface is the same as @cache_result:

view = MyView()
request = factory.get("/items/", QUERY_STRING="q=python")

# Inspect or manipulate the cached response without going through dispatch.
MyView.get.bypass_cache(view, request)
MyView.get.delete_cache(view, request)
MyView.get.refresh(view, request)

See the cache_result guide for the full method surface and async variants.

For per-user caching, partition the cache by request.user.id so each user gets a separate cache namespace.