ASGI, async views/ORM, WebSockets и ограничения async-кода
Статус: актуально для Django 5.2/6.0. Django 6.0 имеет async API не только для части ORM, но и для cache, authentication, sessions и signals. Полностью асинхронные транзакции ORM всё ещё не поддерживаются.
Async позволяет одному процессу обслуживать другие задачи, пока текущая ждёт сеть или другое неблокирующее I/O. Он полезен для fan-out к внешним API, медленных streaming responses и большого числа одновременно ожидающих соединений.
async def не делает CPU-работу параллельной и не ускоряет обычный CRUD автоматически. Если внутри view выполняется синхронный HTTP-клиент, тяжёлое вычисление или длинная синхронная функция, event loop остаётся заблокированным.
Django умеет выполнить async view и под WSGI: для запроса создаётся отдельный event loop. Асинхронные библиотеки внутри view работают, но преимуществ полностью асинхронного request stack и эффективных долгих соединений нет.
Для этого нужен ASGI server:
python -m uvicorn config.asgi:applicationОфициальный вариант с process manager:
python -m gunicorn config.asgi:application \
-k uvicorn_worker.UvicornWorkerОн требует пакеты gunicorn, uvicorn и uvicorn-worker. Точное число процессов, timeouts и пределы соединений выбирают по нагрузочному тесту и инфраструктуре.
Исправлено: стандартный
python manage.py runserverиспользует WSGI application изWSGI_APPLICATION. Наличиеasgi.pyи async view само по себе не переключает его на ASGI. Сторонние пакеты, например Channels, могут переопределить команду.
Файл ASGI создаёт startproject:
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_asgi_application()runserver и любой ASGI/WSGI server разработки нельзя использовать в production без предназначенной для этого конфигурации.
Хороший кандидат одновременно удовлетворяет нескольким условиям:
Один SQL-запрос и рендер шаблона часто не становятся быстрее от async. Обработка изображения, PDF, шифрование большого массива и ML inference блокируют event loop; их переносят в worker/process или специализированный сервис.
Async повышает конкурентность, а не создаёт бесплатную пропускную способность. База, connection pool и внешний API всё равно имеют пределы.
В Python 3.12 TaskGroup удобно выражает группу связанных операций:
import asyncio
import httpx
from django.http import JsonResponse
async def dashboard(request):
timeout = httpx.Timeout(3.0)
async with httpx.AsyncClient(timeout=timeout) as client:
async with asyncio.TaskGroup() as group:
weather_task = group.create_task(
client.get("https://weather.example/api/current")
)
rates_task = group.create_task(
client.get("https://rates.example/api/latest")
)
weather = weather_task.result()
rates = rates_task.result()
weather.raise_for_status()
rates.raise_for_status()
return JsonResponse(
{
"weather": weather.json(),
"rates": rates.json(),
}
)У production endpoint должна быть политика частичного отказа: вернуть весь запрос с ошибкой, показать устаревшее значение или пропустить необязательный блок. Для каждого upstream задают timeout, допустимое число повторов и circuit/bulkhead-пределы. Бесконтрольный fan-out из 100 запросов способен положить downstream быстрее синхронного кода; ограничивайте конкурентность Semaphore или пулом клиента.
Не используйте requests внутри async def. Синхронный сетевой вызов удержит thread event loop до завершения.
Методы QuerySet, выполняющие SQL, обычно имеют вариант с префиксом a: aget(), acreate(), acount(), aexists(), afirst(), aupdate() и другие. QuerySet поддерживает async for.
async def published_titles():
queryset = (
Post.objects.filter(status=Post.Status.PUBLISHED)
.select_related("author")
.order_by("-created_at")[:100]
)
result = []
async for post in queryset:
result.append(
{
"title": post.title,
"author": post.author.display_name,
}
)
return resultМетоды, только изменяющие конструкцию QuerySet, например filter() и select_related(), не выполняют SQL и не требуют await. Ожидать нужно момент вычисления.
Ленивый доступ к незагруженной связи внутри async-контекста попытается выполнить синхронный запрос и вызовет SynchronousOnlyOperation. Подготовьте связи через select_related()/prefetch_related() или выполните весь связанный фрагмент в одной sync-функции.
Не запускайте много ORM-запросов через gather() только потому, что это возможно. Один оптимальный SQL с join/aggregation часто быстрее и бережнее к connection pool.
В Django 5.2/6.0 транзакции ещё не работают как полностью async ORM-контекст. Соберите всю операцию в одну синхронную функцию и пересеките границу один раз:
from asgiref.sync import sync_to_async
from django.db import transaction
@sync_to_async
def reserve_stock(*, product_id, quantity):
with transaction.atomic():
product = Product.objects.select_for_update().get(pk=product_id)
if product.stock < quantity:
raise InsufficientStock
product.stock -= quantity
product.save(update_fields=["stock"])
return product.stock
async def reserve_view(request, product_id):
remaining = await reserve_stock(
product_id=product_id,
quantity=1,
)
return JsonResponse({"remaining": remaining})Не оборачивайте каждую строку цикла отдельным sync_to_async(): это размазывает транзакцию и добавляет переходы. Не передавайте объект connection или cursor между потоками.
sync_to_async() по умолчанию использует thread_sensitive=True, что важно для sync-кода, зависящего от потока. Не переключайте его в False для ORM ради мнимого параллелизма.
При ASGI Django рекомендует отключить persistent connections через CONN_MAX_AGE=0. Вместо соединения, закреплённого за request thread, используйте встроенный pool поддерживаемого database backend или проверенный внешний pool.
Размер пула должен учитывать число ASGI processes и пиковое количество одновременно выполняемых SQL-запросов. Async view способна быстро исчерпать pool и перенести очередь ожидания к базе — это нормальный предел, который нужно измерять.
В актуальном Django доступны, среди прочего:
user = await request.auser()
value = await cache.aget("dashboard:v2")
await cache.aset("dashboard:v2", payload, timeout=60)
await request.session.aset("last_project_id", project_id)Не добавляйте префикс a наугад: сверяйтесь с API конкретного компонента. Некоторые сторонние backends могут выполнять внутреннюю sync-адаптацию, поэтому измеряйте реальное поведение.
Для полностью async request stack каждый middleware должен поддерживать async-вызов. Django адаптирует синхронный middleware через thread, чтобы сохранить совместимость. Это корректно, но при long-polling/SSE или большом числе ожидающих upstream-запросов дополнительный thread на request может уничтожить главное преимущество ASGI.
Включите debug logging django.request и ищите сообщения вида Asynchronous handler adapted for middleware .... Не переписывайте простой middleware ради моды: сначала найдите адаптацию, которая действительно ограничивает конкурентность.
Для долгого запроса клиент может разорвать соединение. Django поднимет asyncio.CancelledError. Освободите прикладные ресурсы и обязательно передайте отмену дальше:
import asyncio
async def stream_report(request):
try:
return await build_streaming_response(request)
except asyncio.CancelledError:
await release_temporary_resources()
raiseНе подавляйте отмену широким except BaseException. Внешние клиенты и собственные корутины тоже должны корректно отменяться.
sync_to_async и async_to_syncsync_to_async(sync_fn) вызывают из async-кода и ожидают через await;async_to_sync(async_fn) вызывают на синхронной границе, например в legacy management command.Внутри уже работающей async-функции async callable нужно просто await. async_to_sync() в том же потоке с event loop не предназначен для этого и может поднять RuntimeError; утверждение, что его нужно использовать для вызова sync Celery .delay(), меняет направление адаптера местами.
Публикация задачи в broker — синхронный I/O конкретной Celery-библиотеки. Если её задержка значима для async endpoint, вынесите вызов в одну sync-функцию через sync_to_async() или используйте поддерживаемый async enqueue API. Саму долгую работу не запускают внутри request coroutine.
Django ASGI поддерживает эффективные async HTTP views и streaming responses, но стандартный Django handler не предоставляет прикладной WebSocket API. Для WebSocket обычно используют Channels или отдельный ASGI-компонент.
В Channels выбирают sync consumer для преимущественно синхронного Django-кода или async consumer с async ORM/database_sync_to_async() для sync database-фрагментов. Authentication и authorization проверяют при соединении и при сообщениях; имя комнаты из URL нельзя без проверки использовать как право подписки.
Channel layer, например Redis, нужен для обмена сообщениями между процессами, но не является долговечным журналом бизнес-событий.
from django.test import TestCase
class DashboardTests(TestCase):
async def test_dashboard(self):
response = await self.async_client.get("/dashboard/")
self.assertEqual(response.status_code, 200)AsyncClient проверяет ASGI-style request path без запуска внешнего сервера. Отдельно тестируйте timeout, частичный отказ, cancellation и предел конкурентности. Нагрузочный тест должен включать реальные latency downstream и ограниченный database pool; benchmark одного пустого endpoint мало что доказывает.
runserver стандартного Django не переключается автоматически на ASGI.DJANGO_ALLOW_ASYNC_UNSAFE не является production-решением SynchronousOnlyOperation; при конкуренции возможна порча данных.Далее: Фоновые задачи: Celery