Async views and the async ORM

Django · Expert · 15 min

Async Django can handle many slow I/O waits efficiently, but only if you understand where it is still synchronous.

Django supports async def views. Under an ASGI server (Uvicorn, Daphne, Hypercorn) they run on the event loop, so one process can wait on many slow network calls at once. Under WSGI, async views still work but each runs in its own one-off event loop, with no concurrency benefit.

The ORM has an async interface: methods prefixed with a (aget, acreate, afirst, acount, aexists, aupdate, adelete, asave), async for obj in queryset, and async related-manager methods. Internally most of these still run the database driver in a thread, so they avoid blocking the loop but aren't faster than sync code.

Calling sync-only code (the regular ORM, many libraries) from an async context raises `SynchronousOnlyOperation`. Wrap it with asgiref.sync.sync_to_async(fn); use async_to_sync for the opposite direction. By default sync_to_async runs functions in a single shared thread (thread_sensitive=True) for safety, which can become a bottleneck.

Limitations to know:

  • transactions don't work in async code; put transactional logic in a sync function and call it with sync_to_async
  • every sync middleware in the stack forces a switch to a thread for each request, losing much of the benefit; check that all middleware is async-capable
  • lazy relationship access (post.author) is a sync query; use select_related or aget

Async views shine for fan-out to external APIs (asyncio.gather with httpx), streaming responses, and long-polling. For typical CRUD pages, sync views under WSGI remain simpler and as fast.

Example

# blog/views.py
import asyncio
import httpx
from asgiref.sync import sync_to_async
from django.db import transaction
from django.http import JsonResponse
from .models import Post

async def post_summary(request, pk):
    post = await Post.objects.select_related("author").aget(pk=pk)
    async with httpx.AsyncClient(timeout=3) as client:
        shares, comments = await asyncio.gather(                 # concurrent I/O
            client.get(f"https://stats.example.com/shares/{pk}"),
            client.get(f"https://stats.example.com/comments/{pk}"))
    tags = [t.name async for t in post.tags.all()]
    await record_view(pk)
    return JsonResponse({"title": post.title, "author": post.author.name, "tags": tags,
                         "shares": shares.json(), "comments": comments.json()})

@sync_to_async
def record_view(pk):
    with transaction.atomic():                                   # transactions: sync only
        post = Post.objects.select_for_update().get(pk=pk)
        post.views += 1
        post.save(update_fields=["views"])

Remember

  • Async views pay off under ASGI for concurrent I/O
  • Use a-prefixed ORM methods and async for; sync ORM calls raise SynchronousOnlyOperation
  • sync_to_async bridges sync code, including transactions
  • Sync middleware in the stack erodes async benefits

Watch out for

  • Accessing a lazy relation like post.author inside an async view without select_related.

Check yourself

  1. Inside an async view, how do you run code that needs transaction.atomic()?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/async/