Working with async views and transactions¶
Use async handlers for awaitable work, but keep a database transaction in a
single synchronous function when using Django's standard ORM. The async ORM
methods adapt sync operations; they do not make atomic() an async context
manager. See Django transactions
and the limitations reference.
One transactional operation, one worker boundary¶
from asgiref.sync import sync_to_async
from django.db import transaction
from django.db.models import F
def reserve_stock(product_id: int, quantity: int) -> int:
if quantity <= 0:
raise ValueError("quantity must be positive")
with transaction.atomic():
product = Product.objects.select_for_update().get(pk=product_id)
if product.available < quantity:
raise InsufficientStock(product_id)
Product.objects.filter(pk=product_id).update(
available=F("available") - quantity
)
reservation = Reservation.objects.create(
product_id=product_id, quantity=quantity
)
return reservation.pk
async def reserve(product_id: int, quantity: int) -> int:
return await sync_to_async(reserve_stock, thread_sensitive=True)(
product_id, quantity
)
The model and domain exception names above belong to the application. Return
an identifier or fully materialized values, not a lazy queryset whose later
evaluation leaves the transaction. Do not use thread_sensitive=False merely
to distribute database calls across more threads.
Atomic writes without a read/modify/write race¶
A single conditional aupdate() with F() can express many counters and
inventory changes without a surrounding application transaction. Check the
affected-row count. For a multi-step invariant or row lock, use the synchronous
unit above. Concurrent sync requests can lose updates too; this is not a race
introduced by the async syntax itself.
ATOMIC_REQUESTS and ATOMIC_SAVE¶
Disable ATOMIC_REQUESTS for the database used by async views. It cannot wrap
an async Django view. aiodrf's ATOMIC_SAVE=True wraps its eligible default
synchronous model save, including ordinary many-to-many writes. It does not
make permission checks, representation, remote calls or arbitrary overridden
save hooks part of the same transaction. Choose an explicit synchronous
perform_create/perform_update operation when the application owns a larger
transactional invariant. Avoid nested transactions solely because both layers
can open one; define which layer owns the unit of work.
External I/O and commit callbacks¶
Do not hold a row lock while awaiting a remote API. Gather external data before
opening the transaction, then recheck database invariants inside it. Publish
notifications after commit using transaction.on_commit. The callback is
synchronous: a blocking Celery publish belongs in the worker, not on the event
loop. A callback failure occurs after commit and cannot roll back the data.
Use a transactional outbox if publication must survive process/broker failure.
Cancellation, connections and tests¶
Client cancellation does not terminate the database thread. Use idempotent
operations and database constraints, not an assumption that a cancelled await
means no write occurred. Configure connection and statement timeouts at the
appropriate driver/database layer. Keep CONN_MAX_AGE=0 for ASGI and size pools
against all workers, replicas and aliases. close_old_connections() is not a
general-purpose async pool-return operation.
Use PostgreSQL transaction tests for locking, rollback and competing updates.
SQLite cannot establish PostgreSQL lock semantics. Tests needing independent
connections should use pytest.mark.django_db(transaction=True) and close their
worker-owned connections before teardown. Keep native backend transaction APIs
in their explicit contrib integration; do not mix a synchronous connection's
transaction with a different async driver's connection.