async def views, await, async middleware, and the ASGI requirement
coreadvancedWriting `async def` in front of a view makes it a coroutine, which lets you `await` inside it. It does not, on its own, make anything faster. Under WSGI, Django runs each async view in its own one-off event loop — you can still `await` concurrent HTTP calls, but you get none of the concurrency benefit of an async stack and you pay roughly a millisecond of context-switching per request. Real concurrency needs ASGI, and the Django docs add a condition people miss: you only get a fully asynchronous stack if **no synchronous middleware is loaded**, because one sync middleware forces a thread switch for every request and can erase the advantage entirely.
Think of it as
The word "async" describes how your code *waits*, not how fast it runs. A coroutine gives up control at an `await` so the event loop can run something else — which is a win only when the thing you are waiting for is I/O that someone else is doing: a network call, a socket, a sleep. It does nothing for CPU work, and it actively hurts if the "wait" is a blocking call, because a blocking call inside a coroutine does not yield, so it stalls the entire event loop rather than one thread. That is the section's own warning, and it is the whole trap: `async def` is a promise you make to the event loop that you will not block, and Python cannot enforce it. The middleware condition follows from the same idea. The request path is a chain, and Django adapts between sync and async at every boundary where the two meet, so one synchronous middleware in a stack of eight means every request crosses into a thread and back — the async view still runs, and the concurrency it was supposed to buy is gone. Check the honest thing rather than the hopeful one: if all your I/O still goes through a blocking driver, `async def` has bought a millisecond of overhead and nothing else.
What we're doing: Fan out to three upstream services concurrently — the case where an async view genuinely wins — with a middleware that does not undo it.
- 2–3
- Declaring both capabilities is what keeps the chain fully async. A middleware marked only `sync_capable` makes Django adapt, and one adaptation per request is enough to lose the benefit.
- 7–9
- `markcoroutinefunction` tells Django this instance is awaitable when it wrapped an async `get_response`. Without it, Django treats the middleware as sync and inserts a thread.
- 27
- An explicit timeout. Without one, a stalled upstream holds the coroutine and the connection indefinitely — async does not add a deadline, it just makes waiting cheaper.
- 28–32
- `asyncio.gather` is where the win is: three upstream calls overlap, so the view takes as long as the slowest rather than the sum. This is the only reason to have written `async def` here.
Why this works: Three sequential 200 ms calls take 600 ms; awaited together they take about 200 ms. That is a real gain, and it exists only because the I/O library is genuinely async and nothing in the chain forced a thread switch.
Making a view `async def` while its I/O stays blocking
Wrong
Better
What you see: Latency is unchanged from the sync version, and under load the whole worker gets *worse* — unrelated endpoints on the same process start timing out while this view waits on an upstream service.
Why: `requests` blocks the thread, and under ASGI that thread is running the event loop. A blocking call never yields, so nothing else on that worker progresses until it returns — one slow upstream call stalls every concurrent connection the worker is holding. `async def` is a promise not to block that the language cannot enforce; keeping it means every I/O library inside must be awaitable.
- Request
- leads to Which server?
- Which server? — WSGI or ASGI — decided at deploy, not in the view
- on error, leads to WSGI: one-off event loop per request (WSGI)
- leads to ASGI: the middleware chain (ASGI)
- WSGI: one-off event loop per request — the view runs; the concurrency does not
- leads to Overhead, and nothing else
- ASGI: the middleware chain — every sync middleware forces a thread switch
- leads to Any synchronous middleware?
- Any synchronous middleware? — one is enough to erase the gain
- on error, leads to Overhead, and nothing else (yes)
- leads to async def view (all async-capable)
- async def view — a promise not to block, which Python cannot enforce
- on error, leads to A blocking call inside it (blocking driver / CPU work)
- leads to await on real async I/O (genuinely awaitable I/O)
- A blocking call inside it — stalls the loop for every other connection on this worker
- leads to Overhead, and nothing else
- await on real async I/O — asyncio.gather over several upstream calls
- leads to Concurrency actually gained
- Concurrency actually gained
- Overhead, and nothing else
What `async def` actually buys, by deployment
Together
Remember: `async def` describes how a view *waits*; it makes nothing faster by itself. Under WSGI each async view gets a one-off event loop, so there is no concurrency gain and about a millisecond of overhead. Under ASGI the gain is real, but only if no synchronous middleware is loaded — one is enough to force a thread switch per request — and only if every I/O call inside is genuinely awaitable, because one blocking call stalls the event loop for every other connection on that worker.
See also: the sync async bridge · async io cancellation and limits · wsgi and asgi as interfaces · custom and async middleware

