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:

from django_fastapi import django_db


@router.get("/books/{book_id}", response_model=BookResponse)
@django_db
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 django_fastapi import database_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 database_sync_to_async(checkout_sync)(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 database_sync_to_async(...), which preserves thread_sensitive=True and returns the thread's database leases in finally.

Connection ownership

Created HTTP apps install a pure ASGI middleware with a separate ThreadSensitiveContext per request. Cleanup runs on that executor before streaming response headers and at completion, including errors and cancellation. Session changes are persisted before headers. This does not automatically clean FastAPI's unrelated AnyIO sync worker threads: decorate ORM-using synchronous handlers and dependencies with @django_db (inside the route decorator). Signatures are preserved, and all initialized Django aliases are covered. django_db accepts ordinary synchronous functions only. It rejects generator (yield) dependencies and async functions at decoration time: their work would otherwise execute after the cleanup boundary had already exited. For a yield dependency, put each short ORM operation in a separate decorated sync helper; do not keep a transaction or lease across the yield.

Use await aclose_db_connections() before a long external await after async ORM work, and in an async stream's finally block. For WebSockets, use database_sync_to_async around each short DB operation; never reserve a connection for the socket's lifetime. None of these helpers closes a connection inside an active atomic block: finish transactions before external waits.

Set CONN_MAX_AGE=0 under ASGI. On supported PostgreSQL/psycopg installations, Django's DATABASES["default"]["OPTIONS"]["pool"] configures a bounded native pool; close then returns a lease rather than necessarily disconnecting TCP. Shared helpers preserve healthy persistent connections when CONN_MAX_AGE is nonzero (for example in WSGI/Celery); only obsolete or unusable ones are closed. The host application owns pool sizing, overload responses, monitoring, and shutdown. The bridge does not create or globally monkeypatch a pool/executor.

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.