Async Django
Since Django 3.0, Django has been steadily adding asynchronous support. It can run under ASGI servers, views can be written with async def, the ORM has an async interface, and middleware, signals, and caching have async-capable paths. Async lets one process handle many concurrent I/O-bound requests, such as those calling external APIs, streaming responses, or holding long-lived connections, using asyncio instead of a thread per request.
Async Django is a tool for specific problems, not a general speed boost. Most CRUD views backed by a database gain little, and mixing sync and async code carelessly can make things slower. Knowing where the sync/async boundaries are, and when to reach for background tasks or Channels instead, is the practical skill.
TL;DR
- Run under ASGI (Uvicorn, Daphne, Hypercorn, or Gunicorn with Uvicorn workers) to benefit from async views.
async defviews are useful for concurrent external calls, streaming, and long-polling; sync views still work under ASGI.- The ORM provides async methods (
aget,acreate,acount,async for), but queries still run in a thread pool around sync drivers. - Cross the boundary with
sync_to_async(call sync code from async) andasync_to_sync(the reverse). - Every sync/async switch has overhead. Keep request paths fully async or fully sync, including middleware.
- For WebSockets use Django Channels; for slow work, use background tasks (Celery, the Django 6.0 tasks framework) rather than async views.
Quick Example
An async view fetching from two external APIs concurrently, plus an async ORM query:
Both API calls run concurrently, so the response takes about as long as the slower call, not the sum of both.
Core Concepts
WSGI vs ASGI
- WSGI is the classic synchronous interface: one request per worker thread or process. Async views still work under WSGI, but Django runs each in its own event loop per request, which gives no concurrency benefit.
- ASGI is the asynchronous interface, and it supports long-lived connections (WebSockets, SSE) and many concurrent requests per worker. Sync views under ASGI run in a thread pool.
Use ASGI if you have async views, streaming, or Channels. Pure sync apps can stay on WSGI (Gunicorn), which is simple and battle-tested.
Async Views
Define a view with async def and Django treats it as a coroutine. Class-based views can define async handler methods (async def get(...)). All handlers in one class must be either sync or async. Good fits:
- Fan-out to external services with
asyncio.gather. - Streaming responses (
StreamingHttpResponsewith an async iterator), such as LLM token streams and SSE. See server-sent events. - Long-polling or waiting on slow upstreams without tying up a thread.
The Async ORM Interface
QuerySet methods that hit the database have a-prefixed async versions: aget, afirst, acount, aexists, acreate, aupdate, adelete, aget_or_create, abulk_create, and so on, plus async for iteration. Model instances have asave() and adelete(). Under the hood, most database backends still use synchronous drivers, so Django runs queries in a thread; you avoid blocking the event loop, but not the thread-pool hop. Transactions (transaction.atomic) aren't yet supported in async code; wrap transactional logic in a sync function called via sync_to_async.
Crossing the Sync/Async Boundary
thread_sensitive=True (the default) runs sync code in a single shared thread, which is safe for code touching the ORM or other non-thread-safe resources, but serialized. Calling sync ORM methods directly inside async code raises SynchronousOnlyOperation, which protects you from blocking the event loop.
Middleware
Middleware can be sync-only, async-only, or support both. If a request passes through sync-only middleware on its way to an async view, Django has to switch contexts, adding overhead per request. For performance-sensitive async paths, make sure your middleware stack is async-capable. Django's built-ins are, but check third-party middleware.
WebSockets and Background Work
Django Channels
Channels extends Django to handle WebSockets and other protocols with consumers (like views for connections) and a channel layer (usually Redis) for sending messages between processes, such as broadcasting chat messages to everyone in a room across multiple servers. It integrates with Django auth and sessions. Use it for chat, live notifications, collaborative features, and real-time dashboards.
Background Tasks Instead of Async
If work is slow but the user doesn't need to wait for it (sending emails, generating reports, processing uploads, calling slow third parties), don't make the request async. Enqueue a background task and respond immediately. Options include Celery, RQ, Dramatiq, django-q2, and Django 6.0's built-in tasks framework, which standardizes task definition with pluggable backends. See background jobs.
When Async Helps
Best Practices
Go Async End to End on Async Paths
Use async HTTP clients (httpx.AsyncClient, aiohttp), async ORM methods, async cache calls, and async-capable middleware. One synchronous requests.get() inside an async view blocks the event loop for every concurrent request on that worker.
Set Timeouts on Every External Call
Async makes waiting cheap, but not free. Unbounded waits pile up connections and memory. Use client timeouts and asyncio.timeout().
Keep Transactions in Sync Functions
Wrap multi-step database writes that need atomicity in a sync function decorated with transaction.atomic, and call it with sync_to_async.
Measure Before Converting
Profile real endpoints. Converting sync views to async without concurrent I/O rarely improves latency, and adds complexity. Convert the specific endpoints that fan out, stream, or wait.
Common Mistakes
Blocking Calls Inside Async Views
Calling Sync ORM Methods in Async Code
Product.objects.get(pk=1) inside an async def raises SynchronousOnlyOperation. Use aget(), or wrap the code with sync_to_async. Lazy relationship access (order.customer.name) also triggers sync queries; select_related it first.
Running Async Views Under WSGI and Expecting Concurrency
Under WSGI, each async view runs in its own event loop per request. It works, but concurrency across requests doesn't improve. Deploy with an ASGI server to benefit.
FAQ
Is Django fully async now?
Not entirely. Views, middleware, the ORM's query interface, caching, and many other APIs support async, but database access still largely runs through synchronous drivers in threads, and transactions aren't yet supported in async code. Support improves with each release.
Should I switch my whole project to ASGI?
If you need async views, streaming, or WebSockets, yes. ASGI servers run sync views fine too. If your app is entirely sync, WSGI with Gunicorn remains a simple, efficient choice, and there's no requirement to switch.
Django async or FastAPI?
FastAPI was async-first from day one and is lightweight for API-only services. Django offers the ORM, admin, auth, migrations, and a huge ecosystem, with async support where it matters. For full-featured web apps, Django is usually more productive; for small, high-concurrency API microservices, FastAPI is often a better fit.
Do I need Channels for Server-Sent Events?
No. SSE works with a plain async view returning a StreamingHttpResponse over an async generator under ASGI. Channels is needed for WebSockets and for cross-process messaging through a channel layer.
Related Topics
- Django — The framework overview
- Python asyncio — The async model underneath
- Django ORM — Sync and async query APIs
- WebSockets — Real-time connections with Channels
- Background Jobs — Offloading slow work
- FastAPI — An async-first Python alternative