# Django Async Views and ASGI in 2026: Performance and Interview Questions > A deep dive into Django async views and ASGI in 2026: how they work under the hood, which server to deploy, the async ORM and SynchronousOnlyOperation trap, plus interview questions. - Published: 2026-07-03 - Updated: 2026-07-06 - Author: SharpSkill - Tags: django, async, asgi, python, performance - Reading time: 10 min --- Django async views let a single worker handle hundreds of concurrent I/O-bound requests without dedicating a thread to each connection, but only when the code path stays asynchronous from the view down to every network call. Since Django 3.1 introduced `async def` views and Django 4.1 added the async ORM interface, the framework has become a credible ASGI platform, yet most production deployments still run under WSGI and leave that concurrency untouched. This deep dive explains how async views behave under ASGI, which servers to run in 2026, the ORM traps that raise `SynchronousOnlyOperation`, and the interview questions that separate engineers who have shipped async Django from those who have only read about it. > **When Async Actually Helps** > > Async views pay off when a request spends most of its time waiting on I/O it does not control: third-party HTTP APIs, slow downstream services, or long-lived streaming responses. For CPU-bound work or fast local database queries, a synchronous view backed by more Gunicorn workers is usually faster and far simpler to reason about. ## How Django Async Views Work Under ASGI WSGI is fundamentally synchronous: each in-flight request occupies one worker thread or process from start to finish, so 50 slow requests need 50 workers. ASGI replaces that model with an event loop that interleaves many requests on a single worker, suspending a coroutine whenever it awaits I/O and resuming it when the data arrives. The [official Django async documentation](https://docs.djangoproject.com/en/5.2/topics/async/) describes the two adapters that make this coexistence possible: under ASGI, Django runs `async def` views natively on the loop and pushes synchronous views into a threadpool with `sync_to_async`; under WSGI, it wraps async views in `async_to_sync` and runs them to completion, discarding every concurrency benefit. The practical consequence is blunt: an `async def` view deployed on a WSGI server behaves like a slower synchronous view. Concurrency only materializes on an ASGI server. A minimal async view that calls an external API looks almost identical to its sync counterpart, with the difference concentrated in the `await`. ```python # views.py import httpx from django.http import JsonResponse async def dashboard(request): # Under ASGI this coroutine runs on the event loop, so the worker is # free to serve other requests while httpx awaits the network round-trip. async with httpx.AsyncClient(timeout=5.0) as client: response = await client.get("https://api.example.com/metrics") return JsonResponse(response.json()) ``` The gain here is not that the single request finishes faster; it takes the same wall-clock time. The gain is that the worker is not blocked during the `await`, so throughput under concurrent load climbs while the process count stays flat. The same model unlocks response patterns that are awkward under WSGI. Since Django 4.2, `StreamingHttpResponse` accepts an async generator, so a view can yield chunks as an upstream source produces them, which suits Server-Sent Events and proxied LLM token streams where the connection stays open for seconds. Persistent bidirectional protocols like WebSockets still route through Django Channels rather than plain HTTP views, but both rest on the same ASGI foundation, so a project that adopts async views has already paid the infrastructure cost for real-time features later. ## Choosing a Django ASGI Server in 2026 Django ships an ASGI application object in `asgi.py`, but it does not run itself; a dedicated ASGI server drives the event loop. The [ASGI specification](https://asgi.readthedocs.io/en/latest/) defines the interface every one of these servers implements, which is why they are interchangeable at the protocol level and differ mainly in performance and protocol coverage. | Server | Implemented in | Best fit | |--------|----------------|----------| | Uvicorn | Python (uvloop) | General ASGI, HTTP/1.1, the common default | | Daphne | Python | Django Channels, WebSockets, HTTP/2 | | Granian | Rust | Highest raw throughput, HTTP/1 and HTTP/2 | | Hypercorn | Python | HTTP/2 and HTTP/3 (QUIC) | For most 2026 deployments, [Uvicorn](https://www.uvicorn.org/) run as a Gunicorn worker class remains the reliable choice: Gunicorn supervises the process lifecycle, restarts crashed workers, and handles signals, while Uvicorn drives the event loop inside each one. One migration detail trips up upgrades: the Gunicorn worker classes moved out of Uvicorn core into a separate `uvicorn-worker` package as of Uvicorn 0.30, so the import path changed. ```bash # Install the worker package first: pip install uvicorn-worker gunicorn myproject.asgi:application \ --workers 4 \ --worker-class uvicorn_worker.UvicornWorker \ --bind 0.0.0.0:8000 ``` Teams chasing maximum throughput increasingly reach for Granian, a Rust-based server that removes the Python-side HTTP parsing overhead. Whatever the choice, the deployment story is the reason async only appears in production when it is deliberately configured; the ASGI-specific setup mirrors the operational care covered in the [Django deployment interview questions](/technologies/django/interview-questions/django-deployment) module. ## The Async ORM and the SynchronousOnlyOperation Trap Django 4.1 added asynchronous query methods across the ORM: `aget`, `acreate`, `asave`, `adelete`, `acount`, `aexists`, `aaggregate`, and iteration with `async for`. These provide an async-friendly surface, but a critical caveat survives into 2026: the database layer underneath is not natively asynchronous. Even with psycopg3, Django executes queries in a threadpool and exposes them through async wrappers, so the async ORM improves ergonomics and avoids blocking the loop without delivering true end-to-end async database I/O. The most common failure mode is calling a synchronous ORM method directly inside an async view. Django detects the async context and refuses, raising `SynchronousOnlyOperation` to prevent a blocking call from freezing the event loop for every other request sharing that worker. > **SynchronousOnlyOperation Is a Feature** > > The exception is a guardrail, not a bug. Reaching for `DJANGO_ALLOW_ASYNC_UNSAFE=true` to silence it reintroduces exactly the blocking behavior async was meant to eliminate. The correct fix is an async ORM method (`aget` instead of `get`) or an explicit `sync_to_async` wrapper around code that has no async equivalent. Async views read cleanly once the async query methods replace their synchronous twins. The pattern below counts rows and streams the first ten records without ever leaving the event loop. ```python # views.py from django.http import JsonResponse from .models import Article async def latest_articles(request): # acount() and async iteration are async-safe wrappers around the ORM. count = await Article.objects.acount() titles = [] async for article in Article.objects.order_by("-created_at")[:10]: titles.append(article.title) return JsonResponse({"count": count, "titles": titles}) ``` Some code has no async counterpart: a third-party SDK that only offers sync calls, or a dense chain of ORM traversals not worth rewriting. Wrapping it in `sync_to_async` from the [asgiref library](https://github.com/django/asgiref) offloads the work to a thread and keeps the loop responsive. The `thread_sensitive=True` argument is the safe default because it routes every wrapped call to one shared executor thread, preserving database connection and transaction state that would break if calls scattered across arbitrary threads. ```python # views.py from asgiref.sync import sync_to_async from .services import build_report # a synchronous function async def report_view(request): # thread_sensitive=True keeps wrapped calls on one shared thread, # preserving connection and transaction state across the boundary. report = await sync_to_async(build_report, thread_sensitive=True)(request.user.id) return JsonResponse(report) ``` ## Django Async Performance: When Concurrency Wins Django async performance is a story about I/O overlap, not raw speed. The clearest win comes from fanning out independent network calls with `asyncio.gather`: three external requests at 200ms each cost roughly 600ms in sequence but complete in about 200ms concurrently, because the awaits overlap on the same loop. ```python # views.py import asyncio import httpx from django.http import JsonResponse async def aggregate(request): async with httpx.AsyncClient(timeout=5.0) as client: # Three independent calls run concurrently instead of one after another. weather, prices, news = await asyncio.gather( client.get("https://api.example.com/weather"), client.get("https://api.example.com/prices"), client.get("https://api.example.com/news"), ) return JsonResponse({ "weather": weather.json(), "prices": prices.json(), "news": news.json(), }) ``` The inverse case matters just as much. A view that runs a single fast local query gains nothing from async and often loses, because every request now pays event-loop scheduling plus a threadpool hop into the sync database driver. Async also imposes a subtler tax at every sync/async boundary: mixing synchronous and asynchronous middleware forces Django to switch between the loop and a thread on each transition, and a request that crosses that line several times per response accumulates real latency. Keeping the middleware stack uniformly async, or uniformly sync, removes those switches. The decision reduces to the shape of the workload rather than a preference for modern syntax. | Workload | Better default | |----------|----------------| | Multiple slow external API calls per request | Async view with `asyncio.gather` | | Long-lived streaming or Server-Sent Events | Async view with an async generator | | Single fast database query, CPU-light | Sync view, more Gunicorn workers | | CPU-bound rendering or computation | Sync view, offload to a task queue | For genuinely long-running jobs, async views are the wrong tool entirely; holding a request open to do minutes of work wastes the connection regardless of the concurrency model. That work belongs in a background worker, a tradeoff explored in the guide on [Celery async tasks in Django](/blog/django/django-celery-async-tasks-interview-2026). Adoption should follow measurement rather than intuition. The honest way to justify async is to load-test the specific endpoint with a tool like Locust or k6 under realistic concurrency, comparing a synchronous version on N workers against an async version on the same hardware. Endpoints dominated by a single external call with high latency show a wide gap in favor of async; endpoints dominated by fast queries frequently show async slightly behind once the threadpool hop is counted. Profiling the request first also reveals whether the bottleneck is even I/O, since async does nothing for a view that is slow because of unindexed queries, a problem addressed in the walkthrough on [optimizing Django ORM queries](/blog/django/django-orm-optimizing-queries). ## Django Async Interview Questions for 2026 A Django async interview probes whether a candidate understands the model or merely recognizes the keywords. The questions below reflect what senior interviews actually ask in 2026, each paired with the answer an experienced engineer gives. **What changes when an async view runs under WSGI instead of ASGI?** Under WSGI, Django wraps the coroutine in `async_to_sync` and runs it to completion on the request thread, so it behaves like a slower synchronous view with zero concurrency benefit. Only an ASGI server running an event loop lets the worker serve other requests during an `await`. **Why does `Article.objects.get(pk=1)` raise `SynchronousOnlyOperation` inside an async view?** The synchronous ORM call would block the event loop, stalling every other request on that worker. Django detects the async context and refuses. The fix is the async method `await Article.objects.aget(pk=1)`, or `sync_to_async` for code with no async equivalent. **Is Django's async ORM truly non-blocking?** No. The async methods are ergonomic wrappers; the underlying queries still run in a threadpool because the database drivers are not integrated for native async execution end to end. The benefit is keeping the loop free, not parallel database I/O at the driver level. **When should async views be chosen over a task queue like Celery?** Async views suit request-scoped I/O concurrency that must finish before the response, such as aggregating several APIs. Work that outlives the request, needs retries, or runs for minutes belongs in Celery or Django's background tasks, since holding an HTTP connection open for it wastes server resources. **What is the cost of mixing sync and async middleware?** Each transition between a sync and an async component forces Django to hop between the event loop and a thread via `sync_to_async` or `async_to_sync`. A request crossing that boundary repeatedly pays cumulative overhead, so a uniform middleware stack performs better than an interleaved one. ## Conclusion - Async views only deliver concurrency on an ASGI server; the same view under WSGI runs synchronously through `async_to_sync`. - Deploy Uvicorn via the `uvicorn-worker` package under Gunicorn, or Granian for maximum throughput, and treat ASGI setup as an explicit deployment step. - Reach for async views when a request waits on multiple slow external I/O calls; keep fast-query and CPU-bound endpoints synchronous with more workers. - Use `aget`, `acreate`, and `async for` inside async views, and remember the ORM still runs queries in a threadpool rather than natively async. - Treat `SynchronousOnlyOperation` as a guardrail: fix it with async methods or `sync_to_async(thread_sensitive=True)`, never by disabling the check. - Keep the middleware stack uniformly sync or async to avoid paying a thread-switch penalty on every boundary crossing. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/django/django-async-views-asgi-2026