An async view can await nonblocking work while an ASGI server handles other connections. That property does not make a blocking database call or CPU-heavy transformation safe on the event loop. Django offers asynchronous variants for many ORM operations, but transactions remain a synchronous boundary in current releases. A function that must lock and commit several rows belongs in one synchronous service called through sync_to_async. The bridge should wrap the whole unit, not one ORM call at a time. Synchronous middleware in an otherwise async request path can require a thread adaptation and reduce the expected concurrency gain. Choosing ASGI therefore depends on the application's actual waiting behavior, not on the presence of async keywords.
Django ASGI, Async Views, and the Sync ORM Boundary
Working case
A permit queue view becomes async because it also waits for a remote jurisdiction status service. Its author reads request.user lazily and calls a synchronous QuerySet directly; the request fails with an async-safety error. A later patch wraps three individual database calls, but the approval transaction is split across awaits and can no longer act as one unit. The repaired read view obtains the user through the async authentication API, bridges once to a synchronous, permission-scoped list service, and awaits the external status call with a deadline. The approval write stays in a separate synchronous transactional service. Only measured overlapping I/O justifies operating this path under ASGI.
Implementation boundary
from asgiref.sync import sync_to_async
from django.http import JsonResponse
from .services import list_authorized_permits
async def permit_queue(request):
reviewer = await request.auser()
if not reviewer.is_authenticated:
return JsonResponse({"error": "Sign in required"}, status=401)
rows = await sync_to_async(
list_authorized_permits, thread_sensitive=True
)(reviewer.id, limit=47)
return JsonResponse({"cases": rows})Use an ASGI application server for the async request path and verify the deployed middleware stack; mixing synchronous middleware can force adaptation. In an async view, await request.auser before reading the authenticated user. Reject an anonymous caller, then call a bounded synchronous service with sync_to_async and thread_sensitive enabled. That service evaluates its QuerySet and returns plain serializable values, so lazy ORM work cannot escape into the async view. Place an explicit timeout around the external network call and define what happens if it fails. For a multi-row approval, keep atomic and on_commit inside one synchronous service. Do not enable an unsafe override to silence an async-safety exception.
Cost and boundaries
ASGI can improve throughput when many requests spend time awaiting genuinely async I/O. A synchronous database bridge still consumes thread capacity and a context switch. Wrapping each model access separately multiplies that overhead and can leak lazy query evaluation into an unsafe context. Fetching 47 summaries at once bounds memory and keeps one permission scope. A remote call needs a deadline, concurrency limit, and fallback contract; without them an async route can accumulate many suspended requests and exhaust downstream capacity. Track event-loop lag, bridge calls per request, thread saturation, external latency, and middleware adaptation before deciding that this path is faster than a conventional synchronous view.
Failure trace
Run the view with anonymous and assigned users. Confirm anonymous access stops before the case query. Deliberately access request.user and evaluate a synchronous QuerySet in an async view in a test, observe the async-safety failure, and restore the safe boundary. Return a lazy QuerySet from the bridged service and show why JSON serialization can trigger work later; replace it with realized dictionaries. Delay the remote status service past its deadline and verify a bounded response. Compare ASGI behavior with a synchronous middleware layer present and removed. Race two approvals to confirm that the separate synchronous transaction still owns the write invariant.
Verification
- Lazy authentication and ORM evaluation never run synchronously on the event loop.
- One bridge returns materialized, permission-scoped values.
- A multi-row transaction remains inside one synchronous service.
Practice drill
Create an async read endpoint that combines a local authorized case list with a remote jurisdiction status. Set a 47-row limit and a finite remote timeout. Place all synchronous ORM evaluation in one service and call it through a single thread-sensitive bridge. Return primitive dictionaries, not models or QuerySets. Measure bridge count and event-loop lag under simultaneous requests. Then implement a distinct synchronous approval service with atomic state and event recording. Test cancellation and timeout, and record which operations can be safely retried after the client loses its response.
Decision note
Use async for overlapping waits; keep transactional ORM work in one synchronous function and bridge at that boundary.
Common Mistakes
- Calling a synchronous QuerySet directly inside an async view.
- Returning lazy models from the bridge for later serialization.
- Adding async syntax without measuring actual waiting or middleware adaptation.
Related lessons
Django Request and Persistence Boundaries; Django Middleware, Sessions, and Object Permissions; Django QuerySet Shape, Prefetch, and Page Cost; Django Forms, CSRF, Atomic Approval, and On-Commit Work; Backend and API Systems; Request Deadlines, Retries, and Backoff.
Apply and check
Build Project: Django permit review service and review Web Development: Django request and persistence quiz.
