Skip to content

Django ORM: sync and async

The ASGI server and the endpoint function are separate choices. Pick the endpoint style based on the code it calls.

Synchronous endpoint

FastAPI runs def operations in a worker thread. Use the normal Django ORM:

@router.get("/books/{book_id}", response_model=BookResponse)
def get_book(book_id: int) -> BookResponse:
    book = Book.objects.select_related("author").get(pk=book_id)
    return BookResponse.from_book(book)

This is often the safest option for established service code, transactions, and third-party Django libraries that are synchronous.

Asynchronous endpoint

Use async ORM methods for every operation that executes SQL:

@router.get("/books/{book_id}", response_model=BookResponse)
async def get_book(book_id: int) -> BookResponse:
    book = await Book.objects.select_related("author").aget(pk=book_id)
    return BookResponse.from_book(book)

Other examples include aexists(), afirst(), acreate(), asave(), and async for item in queryset.

Do not call Book.objects.get() from async def. Django will normally raise SynchronousOnlyOperation to protect the event loop.

Synchronous services from async endpoints

Keep a transactional workflow in one synchronous function and cross the boundary once:

from asgiref.sync import sync_to_async
from django.db import transaction


@transaction.atomic
def checkout_sync(user, payload):
    ...


@router.post("/checkout")
async def checkout(user: AuthenticatedUser, payload: CheckoutPayload):
    return await sync_to_async(checkout_sync, thread_sensitive=True)(user, payload)

Do not set DJANGO_ALLOW_ASYNC_UNSAFE in production. It disables Django's safety check rather than making blocking code asynchronous.

This boundary follows Django's own asynchronous support guidance: keep a transaction or other synchronous workflow inside one synchronous function, then call that function with sync_to_async(..., thread_sensitive=True).

Query planning before serialization

Pydantic may read attributes and properties while creating the response. Load every relation the response mapper needs before entering it:

book = await Book.objects.select_related("author").aget(pk=book_id)
return BookResponse.from_book(book)

For collections, use prefetch_related() before async iteration. Measure query counts in tests so a schema change does not accidentally create an N+1 query.