Async Django can handle many slow I/O waits efficiently, but only if you understand where it is still synchronous.
Django supports async def views. Under an ASGI server (Uvicorn, Daphne, Hypercorn) they run on the event loop, so one process can wait on many slow network calls at once. Under WSGI, async views still work but each runs in its own one-off event loop, with no concurrency benefit.
The ORM has an async interface: methods prefixed with a (aget, acreate, afirst, acount, aexists, aupdate, adelete, asave), async for obj in queryset, and async related-manager methods. Internally most of these still run the database driver in a thread, so they avoid blocking the loop but aren't faster than sync code.
Calling sync-only code (the regular ORM, many libraries) from an async context raises `SynchronousOnlyOperation`. Wrap it with asgiref.sync.sync_to_async(fn); use async_to_sync for the opposite direction. By default sync_to_async runs functions in a single shared thread (thread_sensitive=True) for safety, which can become a bottleneck.
Limitations to know:
- transactions don't work in async code; put transactional logic in a sync function and call it with
sync_to_async - every sync middleware in the stack forces a switch to a thread for each request, losing much of the benefit; check that all middleware is async-capable
- lazy relationship access (
post.author) is a sync query; useselect_relatedoraget
Async views shine for fan-out to external APIs (asyncio.gather with httpx), streaming responses, and long-polling. For typical CRUD pages, sync views under WSGI remain simpler and as fast.
Example
# blog/views.py
import asyncio
import httpx
from asgiref.sync import sync_to_async
from django.db import transaction
from django.http import JsonResponse
from .models import Post
async def post_summary(request, pk):
post = await Post.objects.select_related("author").aget(pk=pk)
async with httpx.AsyncClient(timeout=3) as client:
shares, comments = await asyncio.gather( # concurrent I/O
client.get(f"https://stats.example.com/shares/{pk}"),
client.get(f"https://stats.example.com/comments/{pk}"))
tags = [t.name async for t in post.tags.all()]
await record_view(pk)
return JsonResponse({"title": post.title, "author": post.author.name, "tags": tags,
"shares": shares.json(), "comments": comments.json()})
@sync_to_async
def record_view(pk):
with transaction.atomic(): # transactions: sync only
post = Post.objects.select_for_update().get(pk=pk)
post.views += 1
post.save(update_fields=["views"])Remember
- Async views pay off under ASGI for concurrent I/O
- Use a-prefixed ORM methods and async for; sync ORM calls raise SynchronousOnlyOperation
- sync_to_async bridges sync code, including transactions
- Sync middleware in the stack erodes async benefits
Watch out for
- Accessing a lazy relation like post.author inside an async view without select_related.