skip to content

A synchronous Django service runs under WSGI; how would you decide whether it should move to ASGI and async views?

level: principalimportance: should knowfreq 28%

answer

  1. what the requests wait on
  2. what is still thread-bound
  3. middleware and library audit
  4. one endpoint first, measured

basics

~20 s

Adopt async where requests spend their time awaiting non-database I/O or holding long-lived connections. For ORM-bound CRUD, Django's database work still runs on threads, transactions stay synchronous, and ASGI adds operational change for little gain.

solid answer

~40 s

Start from the workload. Async views pay off when requests wait on **outbound I/O** — fan-out to partner APIs, long-polling, server-sent events — because under ASGI a waiting request costs a coroutine, not a thread. They pay off little for ORM-heavy CRUD: the a-prefixed ORM methods still run the sync ORM through `sync_to_async`, transactions are sync-only, and `ATOMIC_REQUESTS` is incompatible with async views. Then audit the path: sync third-party middleware and decorators each force a thread per request, `CONN_MAX_AGE` should give way to backend pooling, and lazy conveniences such as `request.user` must be rewritten. A sensible plan converts one I/O-bound endpoint, serves it under ASGI (a project has both `asgi.py` and `wsgi.py`), and measures before going further.

go deeper

for a junior

Know that async helps requests that wait on network calls or stay open long, not every Django view.

for a middle

Explain which Django layers remain synchronous underneath: the ORM behind the a-methods, transactions and many third-party middleware.

for a senior

Run the audit: middleware, decorators, lazy request.user and relations, ATOMIC_REQUESTS and connection pooling, before any view is converted.

for a principal

Own the decision with evidence: pick the endpoints whose profile justifies ASGI, decide the deployment split, and define what measurement ends the experiment.

## The question behind the question "Should we go async?" is really three questions: **what do our requests wait on**, **what in our stack is still thread-bound**, and **what does the switch cost to operate**. There is no universal answer; the lead is expected to show the reasoning. ## Where async views pay off Under **ASGI**, a request awaiting I/O occupies a coroutine on the server's event loop rather than a thread. That matters when many requests are in flight and mostly waiting: - **Outbound fan-out** — pricing, shipping-rate or payment calls to partner APIs, several per request. - **Long-lived connections** — server-sent events, long-polling, streaming exports; since 5.0 Django also cancels these cleanly on client disconnect. - **Slow upstreams with spiky concurrency**, where a thread-per-request model would need a very large pool. ## Where they pay off little | Workload trait | Why async adds little in Django | |---|---| | Most time spent in ORM queries | a-prefixed methods such as `aget()` call the sync ORM through `sync_to_async`, so the query still runs on a thread | | Multi-step writes needing transactions | Transactions are not supported in async mode; that code stays sync | | `ATOMIC_REQUESTS` relied on everywhere | Async views raise `RuntimeError` unless they opt out | | Heavy use of generic class-based views | Their `get()` is sync, and handlers must be all async or all sync | | Short requests, modest concurrency | The per-request adaptation cost and new moving parts outweigh the gain | ## The audit before any switch 1. **Middleware**: list every entry in `MIDDLEWARE`. Django's bundled middleware supports both modes; any sync-only third-party middleware forces a thread per request under ASGI and caps the concurrency you came for. 2. **Decorators and libraries**: auth, rate-limiting and HTTP-client libraries must have async paths; a blocking client inside an async view stalls the loop for everyone. 3. **Lazy conveniences**: `request.user`, lazy relations and QuerySets rendered by templates all run sync queries; each becomes `SynchronousOnlyOperation` in async code. 4. **Connections**: the docs say `CONN_MAX_AGE` persistent connections should be disabled in async mode in favour of the backend's pooling (PostgreSQL's `pool` option, 5.1+), sized for in-flight query concurrency. 5. **Tests and tooling**: async views need async test clients and a team comfortable reading event-loop tracebacks. ## A migration shape that limits risk - Keep the service synchronous by default; convert **only the endpoints whose profile justifies it**. - Django generates both `asgi.py` and `wsgi.py`, so the same codebase can be served both ways; one option is to route the streaming or fan-out paths to an ASGI deployment while the rest stays on WSGI. - Measure before and after on the real workload: in-flight requests per process, tail latency, thread counts. The docs themselves say to do your own performance testing and that ASGI is mainly worth it when you have async code. ## Signals you chose well - The converted endpoints hold thousands of waiting connections without a matching thread count. - No "Asynchronous handler adapted for middleware" messages appear on the async path. - Database pool utilisation, not thread count, is the limit you size for. If the honest profile is "mostly ORM reads and writes behind short requests", the right answer is often to stay synchronous and scale workers — and say so. ## A worked decision Take a shipping service whose checkout calls three carrier-rate APIs per request, plus an order-tracking page that polls every few seconds, while the rest is admin-style CRUD. - **Carrier fan-out**: strong candidate — outbound waits dominate, and under ASGI they stop holding threads. - **Tracking updates**: strong candidate if they become a server-sent-events stream, since each open viewer is a long-lived connection. - **CRUD and the admin**: stay synchronous; they wait on the database, rely on `ATOMIC_REQUESTS`-style transactions and generic views, and gain little. The proposal is then two async endpoints behind an ASGI deployment, the rest unchanged, with in-flight concurrency and thread counts compared before and after.

  • Can one Django project serve some endpoints under ASGI and others under WSGI?
    Yes. `startproject` generates both `asgi.py` and `wsgi.py`, and views, sync or async, run under either handler. Teams sometimes route long-lived or fan-out paths to an ASGI deployment of the same code while the bulk stays on WSGI, which limits the blast radius of the change.
  • Why doesn't switching to async views relieve a database bottleneck?
    Because the database work is not async underneath: the a-prefixed ORM methods delegate to the synchronous ORM through `sync_to_async`, so each query still occupies a thread and a connection. The limit is the database and its pool size, which async views do not change.

saying these in an interview costs you the question

  • Async views make ORM-heavy CRUD endpoints substantially faster.
  • Moving to ASGI requires converting every view to async at once.
  • The a-prefixed ORM methods run queries without any threads.
  • Keep CONN_MAX_AGE persistent connections on when moving to async.
  • Third-party middleware needs no review before switching to ASGI.