Django / Correctness and production evidence / Lesson 17
Async views, caching, and work ownership
Choose execution and cache boundaries deliberately without treating async syntax or a cache hit as a guarantee of correctness or durability.
On this page
Lesson goals
- Async is an execution model, not a speed promise
- Execute a bounded async view locally
- Keep cached presentation behind real authorization
Async is an execution model, not a speed promise
Django 5.2 supports async views, with an async-capable stack under ASGI and adaptation under WSGI. The actual benefit depends on middleware, collaborators, and whether the work can await nonblocking operations. CPU-heavy Python or a blocking library call can still occupy the execution thread. Do not convert every function to async because one page is slow. Measure the bottleneck and check the selected deployment path. A direct asyncio test of a view can verify its local result, but it does not prove ASGI throughput, connection management, cancellation, or compatibility with every installed middleware component.
Respect synchronous database safety boundaries
Parts of Django remain synchronous or have specific async interfaces. Use documented async ORM methods where they fit, and do not bypass SynchronousOnlyOperation by setting an unsafe escape hatch merely to silence it. In the 5.2 baseline, transaction work is not simply available as an async atomic block. A synchronous transactional function can be called through the appropriate adapter under its documented thread-sensitive contract. Keep the complete transaction in that function rather than passing connection objects across threads casually. Database connection lifetime and pooling settings also need review for the chosen async deployment, not assumptions copied from a different server arrangement.
Execute a bounded async view locally
The first complete program awaits a synthetic scheduling point and returns a JsonResponse. It makes no network request, starts no server, and uses no database. The result shows that the view can be awaited and inspected through a controlled local call. A real async request that waits on external work also needs deadlines, bounded concurrency, cancellation cleanup, and a policy for an uncertain side effect. Starting a detached background task during a request does not make that task durable; worker shutdown can interrupt it. Use an explicitly supported job system and delivery contract when work must outlive the request.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
import asyncio
import json
from django.http import JsonResponse
from django.test import RequestFactory
async def status(request):
await asyncio.sleep(0)
return JsonResponse({"state": "ready"})
response = asyncio.run(status(RequestFactory().get("/status/")))
print(response.status_code, json.loads(response.content)["state"])
200 ready
The local await demonstrates one async view result. It does not measure a deployed ASGI stack or contact any collaborator.
A cache needs identity, freshness, and invalidation
The second program uses an explicitly scoped local-memory cache to compute a fixed value once across two sequential lookups. Its function-call count is a property of this fixture, not a distributed exactly-once guarantee. LocMemCache is per-process and does not share one authoritative value across application workers. A cache key must include the dimensions that distinguish results, such as tenant or permission scope where relevant, and a timeout must match the freshness policy. Do not mix private responses under a public key. get_or_set does not remove the need to reason about concurrent misses, stampedes, backend availability, or stale values.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.core.cache import cache
from django.test import override_settings
calls = []
def calculate():
calls.append("called")
return "Django"
configuration = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "synthetic-course-cache"}}
with override_settings(CACHES=configuration):
print(cache.get_or_set("public-course:v1", calculate, timeout=30))
print(cache.get_or_set("public-course:v1", calculate, timeout=30))
print(len(calls))
Django
Django
1
Only a process-local synthetic backend is used. Two sequential lookups share the cached result and invoke the calculation once in this fixture.
Keep cached presentation behind real authorization
Caching can accelerate a valid result but cannot authorize it. Scope the data and operation correctly before publication, and design how permission changes invalidate or separate previously cached content. A cached count may be stale even when a current transactional capacity check must be exact. Keep that write invariant in the database rather than accepting a cached availability number as a reservation. Test cache miss, hit, expiry policy, invalidation, and backend failure under bounded fixtures. Use supported Django 5.2 APIs deliberately; this course does not assume newer-version task or async features are present because an unversioned example mentions them.
Common errors and fixes
Async code still blocks other work
- Why it happens
- A synchronous expensive operation runs on the relevant thread.
- How to fix it
- Use a suitable nonblocking interface or explicit execution boundary.
- Prevent it next time
- Measure the actual deployment and collaborator path.
Workers disagree about a supposedly shared cache value
- Why it happens
- LocMemCache is per-process.
- How to fix it
- Choose a reviewed shared backend when the contract requires it.
- Prevent it next time
- Document cache scope independently of key spelling.
A cache hit exposes another tenantโs response
- Why it happens
- The key omitted a required identity or permission dimension.
- How to fix it
- Correct scoping and invalidate unsafe cached content.
- Prevent it next time
- Test separate principals and permission changes.
Check your understanding
1. Does calling an async view under asyncio.run prove efficient production async serving?
Show worked solution
No. It verifies the local coroutine result, while the server protocol, middleware adaptation, connection handling, cancellation, and workload concurrency remain untested. Validate those under an appropriate isolated deployment and measure the intended metric before claiming a performance benefit.
2. Why must an exact seat deduction not rely on a cached availability count?
Show worked solution
The cached value can be stale or differ across workers. Enforce the current capacity rule in the database with the chosen transaction and guarded-write protocol. A cache may inform a display, but it does not reserve inventory or replace the authoritative concurrent transition.
Key takeaways
- Async requires an end-to-end execution review.
- Cache scope and freshness are explicit contracts.
- Background durability needs an owned system.