skip to content

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

level: seniorimportance: should knowfreq 42%

answer

  1. global state, not coroutine-aware
  2. database connections are guarded
  3. lazy objects hide queries
  4. a request-wide transaction setting

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.

solid answer

~40 s

Django marks parts with global, thread-bound state as **async-unsafe**; chiefly the database connection layer. Calling them from a thread with a running event loop raises `SynchronousOnlyOperation`. In an async view that means: the sync ORM (`get()`, `save()`, plain `for` over a QuerySet), `request.user` (a lazy object whose first access queries the session and user tables — use `await request.auser()`, 5.0+), lazy foreign-key access and deferred fields, and a template that iterates an unevaluated QuerySet during `render()`. Setting `ATOMIC_REQUESTS` on a database raises `RuntimeError` for async views, and `transaction.atomic` does not work in async code. The fixes are the a-prefixed ORM methods, prefetching before the data reaches a template, or moving a block of sync ORM work into one function called through `sync_to_async`.

code

python · 10 lines
python
from django.shortcuts import render

from shipping.models import Shipment


async def my_shipments(request):
    user = await request.auser()
    qs = Shipment.objects.filter(owner=user).select_related('carrier')
    shipments = [s async for s in qs]
    return render(request, 'shipping/list.html', {'shipments': shipments})

go deeper

for a junior

Know that the ordinary ORM and request.user cannot be used directly in an async view and that a-prefixed methods and auser() exist.

for a middle

Explain why lazy objects are the usual culprit: relations, deferred fields, request.user and QuerySets evaluated during template rendering.

for a senior

Diagnose from the traceback which lazy access fired, and handle ATOMIC_REQUESTS, transactions and CONN_MAX_AGE correctly for async endpoints.

for a principal

Treat the async-unsafe inventory as a migration cost: every lazy convenience a sync codebase leans on must be made explicit before a view turns async.

## What "async-unsafe" means in Django Some parts of Django keep **global state that is not coroutine-aware** — most importantly the per-thread database connection. Many coroutines share one thread, so letting two of them use that connection at once could interleave queries and corrupt data. Django therefore marks those entry points **async-unsafe**. If one runs in a thread that has a **running event loop**, Django raises `django.core.exceptions.SynchronousOnlyOperation` instead of executing. Two consequences surprise people: - You do not need to be inside `async def` to trip it: a sync helper called directly from an async view runs on the loop's thread and is caught too. - The error often comes from code that does not look like a query at all. ## The inventory an async view has to respect | You touch | Why it fails | Use instead | |---|---|---| | `Order.objects.get(...)`, `order.save()`, `for o in qs` | Sync ORM runs a query | `aget()`, `asave()`, `async for` | | `request.user` | `SimpleLazyObject` loads session and user on first access | `await request.auser()` (5.0+) | | `order.customer` when not loaded | Lazy relation runs a query | `select_related()` / `prefetch_related()` before evaluating | | Deferred fields from `only()` / `defer()` | Loading them queries | Select the fields you need | | `render(request, 'x.html', {'orders': qs})` with an unevaluated `qs` | Template iteration runs the query on the loop thread | Evaluate first with `async for` into a list | | `ATOMIC_REQUESTS = True` | Request handler raises `RuntimeError` for async views | Mark the view with `non_atomic_requests`, or keep that view sync | | `transaction.atomic()` | Transactions are not supported in async mode | One sync function via `sync_to_async` | | Sync-only third-party decorators and middleware | Not written for coroutines | Async-capable versions, or accept the adaptation | ## Reading the two different errors 1. **`SynchronousOnlyOperation`** — the message says you cannot call this from an async context and suggests a thread or `sync_to_async`. The traceback's last Django frame tells you which lazy thing fired: a related descriptor, `SimpleLazyObject`, a template `{% for %}`. 2. **`RuntimeError: You cannot use ATOMIC_REQUESTS with async views.`** — raised by the request handler when it tries to wrap a coroutine view in `transaction.atomic`. It fires for every async view on that database alias unless the view opts out. ## Settings that change in async mode - **`CONN_MAX_AGE`** (default `0`) should stay disabled for async code; the docs recommend the database backend's own pooling instead — on PostgreSQL the `"pool"` option in `OPTIONS`, added in 5.1. - **`DJANGO_ALLOW_ASYNC_UNSAFE`** turns the guard off. The docs reserve it for single-user environments such as a notebook and warn of data loss under concurrency; it is not a fix for a view. ## A diagnosis walk-through When an async view that passed review raises `SynchronousOnlyOperation` in staging: 1. Read the traceback bottom-up to the first frame in your code; that line is where a sync path began. 2. Look one frame below it: a related-object descriptor means a lazy relation, `SimpleLazyObject` means `request.user`, a template node means rendering evaluated a QuerySet. 3. Decide the fix by kind — load it up front, switch to the async API, or move the block into one sync function. 4. Search the rest of the view for the same pattern; the second lazy access is usually one line away from the first. ## A working pattern - Do reads with a-prefixed methods and `async for`, fetching related rows up front. - Hand the template plain lists and model instances, never a live QuerySet. - Group multi-step writes that need a transaction into one synchronous function and cross into it once, instead of wrapping each ORM call separately. - Keep `request.user` out of async code paths; `await request.auser()` gives you the same user through the async auth API. The mechanics of the adapters (`sync_to_async`, `thread_sensitive`) and the full a-prefixed ORM surface belong to their own topics; for an async view the job is to know **which** of Django's everyday conveniences quietly run a query.

  • Your project sets ATOMIC_REQUESTS and you add one async view. What happens and what are your options?
    Every request to that view fails: the handler raises `RuntimeError` saying ATOMIC_REQUESTS cannot be used with async views. Either decorate the view with `transaction.non_atomic_requests` and handle transactions explicitly in a sync function called through `sync_to_async`, or keep that endpoint synchronous.
  • Why does a template rendered from an async view raise SynchronousOnlyOperation when the view itself used only async ORM calls?
    Something the template touches is still lazy: a QuerySet passed unevaluated, or a relation or deferred field on an instance. `render()` runs on the event loop thread, so the query it triggers is caught by the async-safety guard. Evaluate into a list and load relations before rendering.

saying these in an interview costs you the question

  • Only code inside async def can raise SynchronousOnlyOperation.
  • request.user is safe in async views because the middleware already loaded it.
  • Setting DJANGO_ALLOW_ASYNC_UNSAFE is the standard fix for async views.
  • transaction.atomic works in async views as long as you await inside it.
  • Passing a QuerySet to a template is fine because the view used async ORM calls.