skip to content

A Django project has mostly sync views plus a few async views that call slow external APIs; should you serve it with WSGI or ASGI?

level: seniorimportance: should knowfreq 45%

answer

  1. what each mode holds per request
  2. a one-off event loop under WSGI
  3. sync code adapted into threads under ASGI
  4. one sync middleware changes the math
  5. persistent connections vs a pool

basics

~20 s

Under WSGI an async view runs in its own one-off event loop while holding a worker, so awaits inside one view overlap but requests do not. ASGI pays off when async-capable middleware reaches views that stream or wait long on upstream I/O.

solid answer

~40 s

Both work, so decide where concurrency is needed. Under WSGI, Django runs each async view in its own one-off event loop: `asyncio.gather()` over three upstream calls still overlaps them, but the worker is held for the whole request and long-running requests are not efficient. Under ASGI, sync views and sync middleware are adapted into threads with `sync_to_async` at a small per-call cost, and async views can hold many slow connections without a thread each — provided every middleware between server and view is async-capable, since one sync middleware brings back a thread per request. If the async views just parallelise a few calls and return, WSGI stays simpler; if they stream, long-poll or wait long on upstreams, serve with ASGI, audit the middleware, and replace persistent database connections with a pool.

go deeper

for a junior

Know that Django can be served with WSGI or ASGI, that async views run under both, and that ASGI is what long-lived async requests need.

for a middle

Explain the one-off event loop under WSGI, sync_to_async adaptation under ASGI, and why a sync middleware in front of async views costs a thread per request.

for a senior

Make the serving decision from what the async views wait on, audit middleware for adaptation, and move database connections from CONN_MAX_AGE to pooling when choosing ASGI.

for a principal

Weigh one mode for the whole service against splitting streaming paths onto an ASGI deployment, against the team's async discipline and operational cost.

## Two serving modes, one codebase Every Django project has both entry points: `wsgi.py` for WSGI servers and `asgi.py` for ASGI servers. Views may be sync (`def`) or async (`async def`) under either one; Django adapts whichever side does not match. The real question is which mode makes *this mix* of views cheap to run. ## What each mode does with the views | Aspect | Served with WSGI | Served with ASGI | |---|---|---| | sync view | runs directly on the worker thread | adapted into a thread with `sync_to_async` | | async view | runs in its own one-off event loop per request | runs on the server's event loop | | concurrency inside one async view | yes: `asyncio.gather()` overlaps upstream calls | yes | | one worker serving many slow requests at once | no: the worker is busy until the view returns | yes, if the whole middleware chain is async-capable | | streaming, long-polling, server-sent events | works but ties up a worker per connection | the intended use | | persistent DB connections (`CONN_MAX_AGE` > 0) | normal | the docs say to disable them and use pooling | Django's async documentation sums up WSGI plainly: async views work, with a small per-request adaptation cost, but *without the ability to have efficient long-running requests*. ## The cost of mixing Switching between sync and async costs something each time. The Django 6.1 docs put it at **tens of microseconds** per call on the in-request ASGI path — rarely visible against a request measured in milliseconds, but it adds up in tight loops and under thread contention. Two rules follow: - **Middleware decides the ceiling.** If all middleware and views are sync, Django under ASGI switches once, before the middleware stack. If a **sync middleware sits between the ASGI server and an async view**, Django runs that middleware in its own thread and holds the thread for the request, which caps exactly the concurrency ASGI was chosen for. - **Django's bundled middleware supports both modes; third-party middleware may not.** Turn on debug logging for the `django.request` logger and look for *Asynchronous handler adapted for middleware* to see which ones are being adapted. Sync ORM work under ASGI runs in thread-sensitive mode: calls within one request serialise on that request's worker thread, while concurrent requests each get their own. That mirrors Django's connection-per-thread model, which is why the database side needs attention too. ## A decision checklist 1. **What do the async views wait on?** A few upstream calls gathered and returned in well under a second: WSGI is fine and operationally simpler. Streaming responses, long-polling or many seconds of upstream waiting: ASGI. 2. **Is the middleware chain async-capable end to end?** If not, fix or replace the sync pieces before counting on ASGI concurrency. 3. **How are database connections managed?** Under ASGI, set `CONN_MAX_AGE = 0` and use a pool — the PostgreSQL `pool` option since Django 5.1, or an external pooler — sized for concurrent requests. 4. **Does the team have async discipline?** Blocking calls inside `async def` code stall every request on that event loop; Django guards its own async-unsafe parts, but third-party libraries may not. 5. **Measure.** Django's docs recommend load-testing both modes on your own code; a purely sync project can see small gains under ASGI, but the general guidance is to enable ASGI when you have async code that needs it. ## Running both Because the two entry points share one codebase, a common middle path is to keep the bulk of traffic on a WSGI deployment and route the few streaming or long-waiting paths, at the reverse proxy, to an ASGI deployment of the same project. It costs a second deployment but keeps sync-heavy traffic on the simpler model. ## What interviewers listen for That the candidate knows async views *work* under WSGI and what they lose there, that sync middleware undermines an ASGI stack, and that the database connection strategy changes with the serving mode.

  • How do you find out which middleware Django is adapting between sync and async under ASGI?
    Enable debug logging for the `django.request` logger. When Django wraps a middleware to run it in the other mode, it logs a message about an asynchronous handler adapted for that middleware. Each such entry is a sync hop in front of your async views, and a candidate for an async-capable replacement.
  • Under ASGI, why do the docs say to disable persistent database connections?
    Sync ORM work under ASGI runs on per-request worker threads, and each thread keeps its own connection; with `CONN_MAX_AGE` above zero those connections outlive the threads' requests. The docs recommend `CONN_MAX_AGE = 0` plus the backend's pooling, such as the PostgreSQL `pool` option from Django 5.1, which refuses to run alongside a non-zero `CONN_MAX_AGE` anyway.

saying these in an interview costs you the question

  • Async views raise an error when the project is served with WSGI
  • Under WSGI an async view cannot await several calls concurrently
  • Switching to ASGI makes every sync view faster automatically
  • A single sync middleware has no effect on an ASGI deployment
  • Persistent connections with a high CONN_MAX_AGE are the recommended ASGI setup