Async Django Boundaries¶
FastAPI and Celery delivery are async-first. Controllers inherit
BaseAsyncController or BaseCeleryTaskController, public use-case and service
methods are async where they are called from delivery flows, and Django
connection cleanup is handled at the FastAPI request boundary or Celery task
bridge.
The rule¶
Async code may do non-transactional ORM reads with Django's async ORM methods:
# src/fastdjango/core/user/use_cases.py
from fastdjango.core.user.models import User
async def get_user_by_id(self, *, user_id: int) -> User | None:
return await User.objects.filter(id=user_id).afirst()
Django transactions stay sync and go through the injected TransactionFactory.
If a workflow needs a transaction, keep it in a small sync method and call it
from async orchestration with sync_to_async(..., thread_sensitive=True):
# src/fastdjango/core/user/use_cases.py
from asgiref.sync import sync_to_async
from django.contrib.auth.hashers import make_password
from diwire import Injected
from fastdjango.core.user.dtos import CreateUserDTO
from fastdjango.core.user.models import User
from fastdjango.foundation.transactions import TransactionFactory
from fastdjango.foundation.use_cases import BaseUseCase
class UserUseCase(BaseUseCase):
_transaction_factory: Injected[TransactionFactory]
async def create_user(self, *, data: CreateUserDTO) -> User:
return await sync_to_async(
self._create_user_transactionally,
thread_sensitive=True,
)(data=data)
def _create_user_transactionally(self, *, data: CreateUserDTO) -> User:
password = make_password(data.password)
with self._transaction_factory(span_name="create user"):
return User.objects.create(..., password=password)
Never put await inside a Django transaction. Do async/network work before or
after the transaction, or use an outbox/job table when the workflow needs
reliable external side effects.
Django password hashing, password validation, and check_password() are sync
CPU work. Keep them in a sync use-case/service method and call that method with
sync_to_async(..., thread_sensitive=True) instead of running them on the event
loop. Also do password hashing and validation before opening the transaction so
the database transaction does not sit idle while CPU work runs.
Connection handling¶
FastAPI and Celery run without Django's request handler, so the app adds Django
connection cleanup middleware around each HTTP request and WebSocket connection,
and wraps each Celery task handler with the same connection-boundary cleanup.
The middleware also creates an asgiref.sync.ThreadSensitiveContext, matching
Django's ASGI handler, so thread-sensitive ORM work for one ASGI connection
shares one worker thread and connection lifecycle.
DATABASE_CONN_MAX_AGE defaults to 0 for ASGI; use database/backend pooling
rather than Django persistent connections. Docker routes application traffic
through PgBouncer in transaction pooling mode, so
DATABASE_DISABLE_SERVER_SIDE_CURSORS defaults to true.