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

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

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:

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

References