Caching Django model reads in Redis with Encore, Django's built-in Redis cache backend, django-redis or redis-py, with invalidation on write and a Redis that runs locally.
Django pages often repeat the same queries on every request, and Redis is the usual place to keep the results. That adds a Redis server for each developer, a managed Redis in production, a CACHES setting per environment and invalidation code that has to run on every write.
This guide caches a Django model read in Redis with Encore, Django's built-in Redis backend, django-redis and redis-py. Each version reads through the cache and drops the key when the row is renamed.
What you need to decide:
Encore combines Redis databases and Postgres declared in your Django code with the servers behind them, starting both locally under encore run and provisioning ElastiCache on AWS or Memorystore on GCP in your own account on deploy. It is an infrastructure SDK added to the project, so models, the ORM and Django's migrations stay as they are.

The database, secret key and cache are declared in notes/resources.py:
from encore import secrets
from encore.storage import postgres, redis
db = postgres.Database("notes")
django_secret_key = secrets.SharedSecret[str]("DjangoSecretKey")
# Encore starts this under `encore run`; ElastiCache or Memorystore when deployed.
cache = redis.Database("cache")
The detail view reads through the cache with a five minute expiry and falls back to the async ORM on a miss:
import json
from django.http import HttpRequest, JsonResponse
from django.views.decorators.http import require_GET
from .models import Note
from .resources import cache
@require_GET
async def note_detail(request: HttpRequest, note_id: int) -> JsonResponse:
key = f"note:{note_id}"
cached = await cache.string.get[str](key)
if cached is not None:
return JsonResponse({"source": "cache", "note": json.loads(cached)})
note = await Note.objects.filter(pk=note_id).values("id", "title").afirst()
if note is None:
return JsonResponse({"error": "not found"}, status=404)
await cache.string.set(key, json.dumps(note), ex=300)
return JsonResponse({"source": "db", "note": note})
The rename view updates the row and then deletes the cached copy, so the next read goes to Postgres:
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt # Stateless JSON API example without cookie auth.
@require_POST
async def rename_note(request: HttpRequest, note_id: int) -> JsonResponse:
title = json.loads(request.body)["title"]
updated = await Note.objects.filter(pk=note_id).aupdate(title=title)
# Drop the cached copy so the next read sees the new title.
await cache.generic.delete(f"note:{note_id}")
return JsonResponse({"updated": updated})
Migrations run through encore exec, and encore run starts Postgres and Redis locally. The second read comes from the cache, and a rename sends the next read back to the database:
Each request's trace shows the Redis lookup and, on a miss, the SQL query. On deploy, Encore provisions ElastiCache on AWS or Memorystore on GCP, with Postgres as RDS or Cloud SQL, in your own account.
An agent tuning a slow page can add a cached read, then confirm in the local trace that the second request skips the query.
Delete after the write commits; inside transaction.atomic(), use transaction.on_commit so a rolled back update does not clear a valid entry. Keep a TTL as a safety net for writes that bypass the view, such as the admin or a management command.
Django has shipped a Redis cache backend since 4.0:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
}
}
Views then use the standard cache API, including the async variants:
from django.core.cache import cache
note = await cache.aget(f"note:{note_id}")
if note is None:
note = await Note.objects.filter(pk=note_id).values("id", "title").afirst()
await cache.aset(f"note:{note_id}", note, timeout=300)
The async methods currently wrap the synchronous client in a thread. The API covers get, set, delete and counters, not lists or sorted sets, and you run Redis yourself in each environment.
django-redis is the older third-party backend, still common in existing projects:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
}
}
It adds get_redis_connection() for raw commands, compression and custom serializers. Migrating to the built-in backend is usually a settings change, unless the code uses those extras.
For data structures beyond a cache, use the client directly:
import os
import redis.asyncio as aioredis
r = aioredis.Redis.from_url(os.environ["REDIS_URL"], decode_responses=True)
async def top_notes(limit: int = 10) -> list[str]:
return await r.zrevrange("notes:views", 0, limit - 1)
You manage REDIS_URL per environment and the connection pool lifetime under ASGI. Leaderboards and queues fit here better than in the cache API.
Python support in Encore is in beta, and you can sign up for access to try it with your Django project.
Read through the cache in the view and delete the key when the row changes. With Encore, declare cache = redis.Database("cache"), read with await cache.string.get[str](key), store with await cache.string.set(key, value, ex=300), and call await cache.generic.delete(key) after an update. encore run starts Redis locally, and on deploy it becomes ElastiCache or Memorystore.
Yes. Since Django 4.0, django.core.cache.backends.redis.RedisCache is built in and uses redis-py. Set it in CACHES with a LOCATION URL and use the regular cache API.
Delete the key in the code path that writes the row, after the update. Expiry alone means readers can see the old value until it runs out, so pair a TTL with explicit deletes.
Django's built-in backend covers the standard cache API. django-redis adds features such as access to the raw client, compression and pluggable serializers, and it predates the built-in backend.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.