skip to content

Async Support

Django runs async def views under ASGI, bridges sync-only code with asgiref adapters and offers a-prefixed ORM methods. Interviewers probe where async helps and where it silently blocks.

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

explore

questions

14

In Django's async ORM, which QuerySet calls need an a-prefixed version in an async view, and which can you chain as usual?

level: juniorimportance: must knowfreq 52%

answer

  1. does the call hit the database
  2. builders versus evaluators
  3. no afilter
  4. iteration has its own form

basics

~10 s

QuerySet builders such as filter(), exclude() and order_by() run no SQL, so they stay unchanged. Calls that execute a query use awaited a-prefixed versions: aget(), afirst(), acount(), acreate(). Iterate with async for.

solid answer

~40 s

Django splits QuerySet methods into two groups. **Methods that return new QuerySets** — `filter()`, `exclude()`, `order_by()`, `select_related()`, slicing — only build SQL, so they are safe in async code and have no async twin (there is no `afilter()`). **Methods that run a query** have an `a`-prefixed coroutine you must `await`: `aget()`, `afirst()`, `acount()`, `aexists()`, `acreate()`, `aget_or_create()`, `aupdate()`, `adelete()`, `aaggregate()` and more. Model instances add `asave()`, `adelete()` and `arefresh_from_db()`, and related managers add `aadd()`, `aset()`, `aremove()`, `aclear()`. Iteration uses `async for` (or an `async for` comprehension) instead of `for` or `list()`. Forget the `await` and you get coroutine objects where you expected rows.

code

python · 19 lines
python
from django.http import JsonResponse

from notifications.models import Notification


async def notification_summary(request):
    user = await request.auser()
    unread = Notification.objects.filter(recipient=user, read_at__isnull=True)
    count = await unread.acount()
    latest = await unread.order_by('-created_at').afirst()
    items = [
        {'id': n.pk, 'verb': n.verb}
        async for n in unread.order_by('-created_at')[:20]
    ]
    return JsonResponse({
        'unread': count,
        'latest_id': latest.pk if latest else None,
        'items': items,
    })

go deeper

for a junior

Remember the split: builders like filter() stay as they are; anything that runs a query gets an a-prefix and an await, and loops use async for.

for a middle

Explain why the split follows laziness, list the model-instance and related-manager a-methods, and spot a missing await from its symptoms.

for a senior

Catch the hidden sync evaluations in review: list(), len(), templates, lazy relations and deferred fields read after an async query.

for a principal

Set team conventions for async ORM code: one await per expression at the evaluating call, and no QuerySets leaving the view unevaluated.

## The rule: does this call touch the database? Django's QuerySet is **lazy**: building one runs no SQL; SQL runs when the QuerySet is evaluated. In async code that distinction decides everything, because running a query with the synchronous API from an async context is blocked by Django's async-safety guard. The docs give the practical rule: look up the method in the QuerySet reference, where methods are grouped into those that **return new QuerySets** (non-blocking, no async version) and those that **do not return QuerySets** (blocking, with an `a`-prefixed async version). ## The two groups | Group | Examples | In async code | |---|---|---| | Build a QuerySet | `filter()`, `exclude()`, `order_by()`, `select_related()`, `prefetch_related()`, `values()`, `annotate()`, slicing `[:20]` | Chain freely, no `await` | | Run a query, return a value | `get()`, `first()`, `last()`, `count()`, `exists()`, `contains()`, `aggregate()`, `in_bulk()`, `earliest()`, `latest()`, `explain()` | `await qs.aget()`, `afirst()`, `alast()`, `acount()`, `aexists()`, `acontains()`, `aaggregate()`, `ain_bulk()`, `aearliest()`, `alatest()`, `aexplain()` | | Write | `create()`, `get_or_create()`, `update_or_create()`, `bulk_create()`, `bulk_update()`, `update()`, `delete()` | `acreate()`, `aget_or_create()`, `aupdate_or_create()`, `abulk_create()`, `abulk_update()`, `aupdate()`, `adelete()` | | Iterate | `for n in qs`, `list(qs)`, `iterator()` | `async for n in qs`, `[n async for n in qs]`, `aiterator()` | Managers expose the same methods, so `await Notification.objects.aget(pk=pk)` works directly. ## Beyond the QuerySet - **Model instances** (since 4.2): `await obj.asave()`, `await obj.adelete()`, `await obj.arefresh_from_db()`. Since 6.0 `asave()` accepts keyword arguments only, matching `save()`. - **Related managers**: `aadd()`, `aremove()`, `aclear()`, `aset()` (added in 4.2) on many-to-many and reverse foreign-key managers, plus `acreate()`, `aget_or_create()` and `aupdate_or_create()`. - **Shortcuts** (since 5.0): `aget_object_or_404()` and `aget_list_or_404()`. - **Return values match the sync versions**: `aget_or_create()` still returns an `(object, created)` tuple, and `aget()` still raises `DoesNotExist` or `MultipleObjectsReturned`. ## Mistakes that show up in review 1. **Missing `await`.** `latest = qs.afirst()` stores a coroutine; the docs mention errors like "coroutine object has no attribute" or `<coroutine …>` strings in templates. 2. **Evaluating with sync tools.** `list(qs)`, `len(qs)`, `bool(qs)` and a plain `for` all run the query synchronously and are refused in async code. 3. **Hunting for `afilter()`.** It does not exist because `filter()` never touches the database. 4. **Lazy attributes after the fact.** A foreign key or deferred field read on an instance loaded in async code runs a hidden query; load it up front. 5. **Mixing in a transaction.** `transaction.atomic()` does not work in async code; transactional work belongs in a sync function. ## Translating a sync view line by line A sync notifications view converts mechanically once you apply the rule: 1. `Notification.objects.get(pk=pk)` becomes `await Notification.objects.aget(pk=pk)`. 2. `qs.count()` becomes `await qs.acount()`; `qs.exists()` becomes `await qs.aexists()`. 3. `for n in qs:` becomes `async for n in qs:`; `list(qs)` becomes `[n async for n in qs]`. 4. `n.save(update_fields=['read_at'])` becomes `await n.asave(update_fields=['read_at'])`. 5. `get_object_or_404(Notification, pk=pk)` becomes `await aget_object_or_404(Notification, pk=pk)`. 6. `Paginator(qs, 20)` becomes `AsyncPaginator(qs, 20)` (6.0+), whose page and count methods are awaited. Lines that only build QuerySets — `filter()`, `order_by()`, `select_related()` — are copied across untouched. ## Reading one line correctly `await user.notifications.filter(read_at__isnull=True).order_by('-created_at').afirst()` has exactly one database round-trip — at `afirst()` — and exactly one `await`, placed in front of the whole expression. Everything before it is QuerySet building. Putting the `await` in front of the expression, not in front of `filter()`, is the habit that makes async ORM code read cleanly.

  • What does aget_or_create() return in Django, and does it differ from get_or_create()?
    It returns the same `(object, created)` tuple, so you still unpack it: `obj, created = await Model.objects.aget_or_create(...)`. It wraps the synchronous `get_or_create()`, so defaults handling and the retry-on-`IntegrityError` behaviour are identical.
  • Why does passing an unevaluated QuerySet to a template from an async view fail even though no await is missing?
    The template's loop evaluates the QuerySet synchronously during rendering, on the event loop's thread, so Django's async-safety guard raises `SynchronousOnlyOperation`. Evaluate it first with an `async for` comprehension and pass the list.

saying these in an interview costs you the question

  • Every QuerySet method needs an a-prefixed version in async code, including filter().
  • list(qs) is a safe way to evaluate a QuerySet inside an async view.
  • aget_or_create() returns just the object, unlike get_or_create().
  • Model instances have no async save, so writes need raw SQL in async views.
  • Forgetting await on afirst() still returns the row, only more slowly.
open as a page

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%

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.

open as a page

In Django, what do asgiref's sync_to_async() and async_to_sync() do, and when do you reach for each?

level: middleimportance: must knowfreq 55%

basics

~20 s

sync_to_async wraps a sync function so async code can await it, running it on a worker thread. async_to_sync wraps a coroutine function so sync code can call it and block for the result. Django uses both internally.

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 Django, why does an async view that calls Invoice.objects.get() raise SynchronousOnlyOperation, and how do you fix it?

level: juniorimportance: should knowfreq 48%

basics

~20 s

Django guards its database layer as async-unsafe: when a guarded call runs in a thread with a running event loop, it raises SynchronousOnlyOperation. Use the async ORM API or wrap the sync code in sync_to_async; never DJANGO_ALLOW_ASYNC_UNSAFE.

open as a page

In Django, what does await Notification.objects.aget(pk=pk) actually do underneath, and what does that mean for performance?

level: middleimportance: should knowfreq 40%

basics

~10 s

It awaits sync_to_async(self.get): the ordinary synchronous get() runs on a worker thread with a synchronous database driver. The async ORM frees the event loop, but each query still occupies a thread and a connection.

open as a page

In a Django async view, why is wrapping each row's ORM call in sync_to_async a poor design, and how should you restructure it?

level: seniorimportance: should knowfreq 30%

basics

~10 s

Every sync_to_async crossing pays a context switch, and per-row crossings multiply it and split the work into separate autocommit steps. Put the whole loop, and any transaction, in one sync function and cross once.

open as a page

When a Django async view calls a sync-only SDK through sync_to_async, what does thread_sensitive=True do, and when would you pass False?

level: seniorimportance: should knowfreq 38%

basics

~20 s

With thread_sensitive=True, the default, the call runs on one shared thread per request context, so thread-bound resources like Django's database connection stay valid. Pass False for thread-safe code that never touches the ORM, so calls can run concurrently.

open as a page

In a Django async view looping over notifications with async for, why does reading notification.actor raise SynchronousOnlyOperation, and how do you avoid it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

notification.actor is a lazy foreign key: the first read runs a synchronous query, which Django refuses in async code. Load it up front with select_related('actor') or prefetch_related(), or fetch it explicitly with an awaited aget().

open as a page

With no async transaction.atomic in Django, which async ORM calls are still atomic on their own, and what do you do when you need more?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Each a-method runs its sync twin in one thread call, so anything that call does atomically stays atomic: aupdate() is one UPDATE, adelete() and aget_or_create() use transactions internally. Several steps together need one sync function with transaction.atomic(), awaited once.

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

In Django, does async for over a QuerySet stream rows from the database, and when should you use aiterator() instead?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

No. async for fetches every row into the result cache in one synchronous call, then yields them. aiterator() fetches in chunks (2000 by default), skips the cache and can use server-side cursors, suiting large read-once results.

open as a page