When Async Django Views Pay Off in Production
Decide when async views improve a production Django app: concurrent external API calls, streaming, and long-polling benefit, while ORM-heavy views usually do not. Measure, avoid blocking calls, and run async code under ASGI correctly.
Problem statement
Django supports async def views, an async ORM interface, and ASGI servers. It is tempting to convert views to async expecting a speed-up. For a typical view that runs a few database queries and renders a template, async brings little or nothing, and can be slightly slower because sync code runs in threads. Async pays off in specific situations: waiting on several slow external services at once, holding many concurrent connections open, or streaming responses. Knowing which views fit, and measuring, avoids complexity without benefit.
- Async helps views that wait: several HTTP calls to external APIs, LLM or search services, long-polling, server-sent events, WebSockets.
- Async rarely helps views dominated by database queries and Python work; the ORM runs in a thread pool anyway.
- Run async views under ASGI (Uvicorn workers, Granian, Daphne); under WSGI they work but run in a per-request event loop with no benefit.
- Use async clients (
httpx.AsyncClient) andasyncio.gatherfor concurrent calls. - Never call blocking code in async views; use the async ORM (
aget,async for) orsync_to_async. - Measure latency and throughput before and after on realistic traffic.
Step-by-step solution
1. Find candidate views
Look at request traces or logs for views where most time is spent waiting on external services: payment providers, shipping rates from several carriers, AI completions, aggregating data from multiple internal APIs. A view that calls three APIs sequentially at 300 ms each takes 900 ms; running them concurrently takes about 300 ms.
2. Rewrite with concurrent I/O
import asyncio
import httpx
from django.http import JsonResponse
CARRIERS = ["https://api.carrier-a.example/rates", "https://api.carrier-b.example/rates", "https://api.carrier-c.example/rates"]
async def shipping_quotes(request):
params = {"zip": request.GET["zip"], "weight": request.GET["weight"]}
async with httpx.AsyncClient(timeout=5) as client:
responses = await asyncio.gather(*(client.get(u, params=params) for u in CARRIERS), return_exceptions=True)
quotes = [r.json() for r in responses if isinstance(r, httpx.Response) and r.status_code == 200]
return JsonResponse({"quotes": quotes})
return_exceptions=True keeps one failing carrier from failing the whole view. In production, reuse a client across requests rather than creating one per request.
3. Run under ASGI
gunicorn config.asgi:application -k uvicorn_worker.UvicornWorker --workers 4
Under WSGI, Django runs each async view in its own event loop for that request, so there is no concurrency across requests and some overhead. Under ASGI, a worker's event loop serves other requests while one view awaits.
4. Keep the event loop unblocked
Any blocking call (sync ORM access, requests.get, time.sleep, heavy CPU work) inside an async view stalls every request on that worker. Django guards the ORM with SynchronousOnlyOperation; other libraries have no guard. Use:
order = await Order.objects.select_related("customer").aget(pk=pk)
items = [i async for i in order.items.all()]
result = await sync_to_async(legacy_pricing)(order.pk)
5. Mixed apps are fine
Under ASGI, sync views still work: Django runs them in a thread pool (with thread_sensitive defaults). Convert only the views that benefit. Middleware should be async-capable to avoid extra context switches; most of Django's built-in middleware is.
6. Measure
Load-test the converted endpoints and compare p50, p95, and throughput, plus CPU and memory per worker. If gains are small, revert: sync code is simpler to debug.
Verification check: compare a load test of the endpoint before and after (for example 50 concurrent users) and confirm lower p95 latency and no SynchronousOnlyOperation errors in logs; check that worker CPU did not rise significantly.
Explanation
An event loop lets one thread make progress on many tasks by switching between them whenever one awaits I/O. A view that awaits several network calls at once overlaps their waiting time, and the worker serves other requests while those calls are in flight. Django's database layer is still synchronous underneath (the async ORM methods run queries in a thread pool), so views dominated by database work do not get faster by becoming async. The benefit is therefore specific to I/O concurrency, not a general speed-up.
Edge cases / notes
- Timeouts. Always set timeouts on async HTTP clients; a hung call holds the coroutine indefinitely.
- Connection pools. Reuse
httpx.AsyncClientinstances; creating one per request wastes TLS handshakes. - Testing uses
django.test.AsyncClientandasync deftest methods. - Celery remains the right tool for work that does not need to finish within the request.
Next steps
- Understand the server side in Django WSGI vs ASGI.
- Fix blocking-call errors with Fix Django SynchronousOnlyOperation.
FAQ
Are async Django views faster?
Only when the view waits on I/O concurrently. ORM-heavy views see little or no gain.
Do async views need an ASGI server?
To benefit, yes. Under WSGI they run but without concurrency across requests.
Can I mix sync and async views?
Yes. Under ASGI, Django runs sync views in a thread pool.
Is the Django ORM async?
It has an async interface (aget, async for), but queries still run in threads underneath.