skip to content

Coroutine Views & ASGI

async def views and async class-based view handlers pay off under ASGI; under WSGI each call gets a one-off event loop. Interviewers check you know when async buys real concurrency.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django, how do you write an async view, both as a function-based view and as a class-based view?

level: juniorimportance: must knowfreq 58%

answer

  1. what the callable returns
  2. async def on the right methods
  3. not as_view or __init__
  4. handlers all one colour

basics

~20 s

Declare a function-based view with async def. For a class-based view, declare its HTTP handlers such as get() and post() with async def, leaving as_view() alone; the handlers must be all async or all sync.

solid answer

~30 s

Django treats a view as async when its callable is a coroutine function, which it checks with `inspect.iscoroutinefunction`. For a function-based view you write `async def my_view(request)` and `await` inside it. For a class-based view you make the HTTP method handlers (`get()`, `post()`, `put()`…) `async def`; `as_view()` and `__init__()` stay ordinary. `View.view_is_async` inspects the handlers when `as_view()` runs, raises `ImproperlyConfigured` if they mix sync and async, and marks the returned callable as a coroutine function. `options()` and the 405 handler adapt automatically. The URLconf is unchanged: the same `path()` routes both kinds.

code

python · 24 lines
python
import asyncio

from django.http import JsonResponse
from django.urls import path
from django.views import View


async def carrier_ping(request):
    await asyncio.sleep(0.05)  # stands in for awaited I/O
    return JsonResponse({'status': 'ok'})


class ShippingQuoteView(View):
    async def get(self, request, carrier):
        return JsonResponse({'carrier': carrier})

    async def post(self, request, carrier):
        return JsonResponse({'carrier': carrier, 'accepted': True}, status=201)


urlpatterns = [
    path('carriers/ping/', carrier_ping),
    path('quotes/<str:carrier>/', ShippingQuoteView.as_view()),
]

go deeper

for a junior

Recall the two forms: async def for a function view, async def on get() and post() for a class-based view, with the URLconf unchanged.

for a middle

Explain how Django detects an async view with iscoroutinefunction, and why view_is_async rejects mixed handlers when as_view() runs.

for a senior

Show you know where the rule bites in practice: generic views with a sync get(), decorators that predate async, and a forgotten await surfacing as a ValueError.

for a principal

Frame the cost of async CBVs honestly: generic views stop helping, so an async surface usually means plain View subclasses or function views.

## What makes a view async A Django **view** is any callable that takes an `HttpRequest` and returns an `HttpResponse`. A view becomes **async** when that callable is a **coroutine function**: calling it returns a coroutine that Django then awaits. Django decides by calling `inspect.iscoroutinefunction` on the callable it resolved from the URLconf. There is no separate registration, no special URL helper and no setting to flip: `path('quote/', views.quote)` routes a sync view and an async view the same way. If you produce a coroutine some other way (for example a callable object whose `__call__` returns a coroutine), the docs ask you to mark it with `inspect.markcoroutinefunction` so the check returns `True`. ## Function-based views For a **function-based view** the whole function is declared with `async def`: - you may `await` coroutines inside it, including Django's async APIs such as `request.auser()` (5.0) and `aget_object_or_404()` (5.0); - it must still **return** an `HttpResponse` (or subclass) — not a coroutine; - if you forget an `await` and return a coroutine object, Django's `check_response` raises `ValueError` saying the view "returned an unawaited coroutine instead. You may need to add an 'await' into your view." ## Class-based views: all or none For a **class-based view** (CBV) you do not make the class async; you make its **HTTP method handlers** async — `get()`, `post()`, `put()`, `patch()`, `delete()` and so on. The docs are explicit that `__init__()` and `as_view()` stay ordinary methods. The rule that surprises people is **all or none**: 1. When `as_view()` runs (that is, when the URLconf is imported), Django evaluates the class property `View.view_is_async`. 2. It collects every handler named in `http_method_names` that the class defines, except `options`. 3. If some are coroutine functions and some are not, it raises `ImproperlyConfigured` with the message that the class's "HTTP handlers must either be all sync or all async". 4. If they are all async, it marks the view function returned by `as_view()` as a coroutine function, so the request handler awaits it. `options()` and `http_method_not_allowed()` are written to return an awaitable when `view_is_async` is true, so you do not have to override them. `head()` falls back to your `get()`, so it inherits its colour. The all-or-none rule bites hardest with the **generic views**: `TemplateView`, `ListView`, `DetailView`, `FormView` and friends define a synchronous `get()`. Adding an `async def post()` to a `ListView` subclass therefore mixes colours and fails at import time. You would have to override `get()` as `async def` too, and at that point most of the generic view's sync machinery (which evaluates QuerySets synchronously) no longer helps you. ## Decorators on async views | Decorator family | Async views supported | |---|---| | `never_cache`, `cache_control`, `csrf_exempt`, `csrf_protect`, `require_POST`, `require_http_methods`, `vary_on_headers`, `gzip_page`, `etag`… | Yes, since 5.0 | | `login_required`, `permission_required`, `user_passes_test` | Yes, since 5.1 | | Third-party decorators | Only if written for it | `method_decorator` works on `async def` handlers. If you decorate `dispatch()` on a view with async handlers, the docs show overriding `dispatch()` as `async def` so the decorated method is still marked as a coroutine function. ## Sync and async views side by side The choice is made **per view**, not per project. One URLconf can route a sync `DetailView` next to an async function view and an async `View` subclass; the request handler adapts each one individually. That makes adoption incremental: - keep existing sync views untouched; - write new I/O-heavy endpoints as async views; - inside an async view, `await` async-native work and reach Django's database layer only through its async API or an explicit adapter. What an async view does **not** change is the contract around it: it receives the same `HttpRequest`, returns the same response classes, is reversed with the same `reverse()` names, and is wrapped by the same middleware stack (adapted where needed). ## Common first mistakes - Making `as_view()` or `__init__()` async and expecting the class to become async. - Mixing an `async def get()` with a `def post()` and meeting `ImproperlyConfigured` at startup. - Calling the synchronous ORM (`Order.objects.get(...)`) from inside the async view — Django's async-safety guard stops that; the a-prefixed ORM methods and the sync adapters are the fix. - Returning `some_async_helper(request)` without `await`. An async view runs under both WSGI and ASGI deployments; what differs is how much concurrency it actually buys, which is a separate question.

  • Can you add an async post() to a subclass of Django's ListView?
    Not on its own. `ListView` inherits a synchronous `get()` from `BaseListView`, so adding `async def post()` mixes sync and async handlers and `view_is_async` raises `ImproperlyConfigured` when `as_view()` runs. You would have to override `get()` as `async def` as well, which means rewriting the part of the generic view that evaluates the QuerySet synchronously.
  • Do Django's built-in view decorators work on async views?
    Most do. Since 5.0 the cache, CSRF, HTTP-method, conditional, vary, gzip and X-Frame-Options decorators accept both sync and async functions, and since 5.1 `login_required`, `permission_required` and `user_passes_test` do too. Third-party decorators only work if they were written for coroutine functions, and `method_decorator` on `dispatch()` needs an `async def dispatch()` when the handlers are async.

saying these in an interview costs you the question

  • Making as_view() or __init__() async is what turns a class-based view async.
  • A class-based view can freely mix an async get() with a sync post().
  • Async views need a special async URL helper instead of path().
  • An async view only works when the project is deployed under ASGI.
  • Django silently awaits a coroutine that a view returns by mistake.
open as a page

A Django async view calls three shipping-rate APIs concurrently; what does it gain under WSGI, and what more under ASGI?

level: middleimportance: must knowfreq 52%

basics

~20 s

Under both, asyncio.gather overlaps the three calls, so that request takes roughly as long as the slowest one. Only under ASGI does the process serve other requests while it awaits; under WSGI it holds a worker for the whole request.

open as a page

In a Django async view, which parts of Django fail when touched directly, and what do you use instead?

level: seniorimportance: should knowfreq 42%

basics

~10 s

Anything that runs a synchronous database query: the sync ORM, lazy request.user, lazy relations and QuerySets evaluated in templates raise SynchronousOnlyOperation. ATOMIC_REQUESTS raises RuntimeError. Use a-prefixed ORM methods, request.auser(), prefetching, or sync adapters.

open as a page

In Django under ASGI, how do you stream updates from an async view with StreamingHttpResponse and clean up when the client disconnects?

level: seniorimportance: should knowfreq 34%

basics

~10 s

Pass an async generator to StreamingHttpResponse. When the client disconnects, Django (5.0+) cancels the coroutine serving the response, so asyncio.CancelledError is raised inside the generator; catch it, release resources, and re-raise.

open as a page

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%

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.

open as a page