Django Channels в проде: чек-лист перед запуском вебсокетов
Вебсокеты на Django Channels отлично работают локально и падают под нагрузкой в проде. Разбираем, что проверить в конфигурации ASGI, Redis и Nginx до того, как соединения начнут рваться.

Локально всё работает: runserver поднимает ASGI, вебсокет подключается, сообщения летят. В проде на первой же сотне одновременных соединений начинаются разрывы, сообщения теряются, а Redis отъедает память до OOM. Причина почти всегда не в коде консьюмеров, а в обвязке: неправильный сервер, дефолтный channel layer, Nginx, который рвёт idle-соединения через 60 секунд. Этот чек-лист — то, что стоит проверить в связке Django Channels 4.x и channels_redis 4.x перед тем, как открывать вебсокеты на живых пользователей.
Формат — практический чек-лист по слоям: транспорт, channel layer, деплой, устойчивость соединений, безопасность. Внутреннее устройство протокола ASGI и написание самих консьюмеров здесь не разбираем — только то, что ломается на переходе от runserver к реальному серверу.
Транспорт: чем поднимать ASGI
python manage.py runserver в проде использовать нельзя — это касается и WSGI, и ASGI. Для Channels нужен ASGI-сервер: Daphne (родной для проекта) или Uvicorn с воркерами. Gunicorn сам ASGI не обслуживает, но умеет управлять Uvicorn-воркерами через класс uvicorn.workers.UvicornWorker.
Ключевой момент, на котором спотыкаются: вебсокет-соединение живёт всё время, пока открыт сокет, и занимает один асинхронный слот. Это не короткий HTTP-запрос. Синхронный воркер под каждое соединение держать нельзя — тысяча пользователей положит сервер. Поэтому либо асинхронные консьюмеры, либо очень аккуратная работа с SyncConsumer и пулом потоков.
# Запуск Daphne 4.x напрямую
daphne -b 0.0.0.0 -p 8001 myproject.asgi:application
# Или Uvicorn 0.30.x под управлением Gunicorn 22.x
gunicorn myproject.asgi:application \
-k uvicorn.workers.UvicornWorker \
-b 0.0.0.0:8001 \
--workers 4
Проверка: подключитесь к ws://ваш-сервер:8001/ws/... внешним клиентом (например, websocat) в обход Nginx. Если соединение встаёт — транспорт живой, дальше ищем проблему в прокси.
Channel layer: убрать InMemory
Самая частая ошибка перед продом — оставить InMemoryChannelLayer. Он держит группы и сообщения в памяти одного процесса. Как только у вас больше одного воркера (а в проде их всегда больше одного), пользователи, подключённые к разным процессам, перестают видеть сообщения друг друга: групповая рассылка доходит только до тех, кто попал в тот же процесс.
В проде channel layer — это Redis через channels_redis. Он становится общей шиной между всеми воркерами.
# settings.py, Channels 4.x + channels_redis 4.x
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
# ограничиваем размер очереди на канал,
# чтобы медленный клиент не копил сообщения бесконечно
"capacity": 100,
# время жизни сообщения в очереди, секунды
"expiry": 10,
},
},
}
Два параметра, о которых забывают, и потом ловят рост памяти Redis:
- capacity — сколько сообщений висит в очереди одного канала, пока их не забрали. Если клиент отключился, а сообщения ему шлют, очередь копится до этого предела. По умолчанию сто, но под ваши объёмы значение стоит осознанно подобрать.
- expiry — через сколько секунд неполученное сообщение выбрасывается. Дефолт 60 секунд означает, что "мёртвые" сообщения живут в Redis до минуты.
InMemoryChannelLayer в документации прямо помечен как решение для разработки и тестов. Если он дожил до прода — это не оптимизация, а отложенный инцидент: рассылки будут работать через раз в зависимости от того, на какой воркер попал пользователь.
Точные имена бэкендов проверяйте
Строка BACKEND должна совпадать с путём в установленной версии channels_redis. В 4.x это channels_redis.core.RedisChannelLayer и отдельный RedisPubSubChannelLayer для pub/sub-режима. Если обновляете пакет — сверьтесь с README репозитория пакета, путь и набор опций между мажорными версиями менялись.
Nginx: idle timeout и Upgrade
Nginx по умолчанию не проксирует вебсокеты и рвёт неактивные соединения. Два блока, без которых ничего не заработает: заголовки апгрейда и таймауты.
location /ws/ {
proxy_pass http://127.0.0.1:8001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# без этого Nginx закроет idle-сокет через 60 секунд
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
Симптом, который выдаёт проблему: вебсокет исправно работает, но ровно через минуту молчания клиент получает разрыв. Это дефолтный proxy_read_timeout 60s. Лечится либо увеличением таймаута, либо приложенческим heartbeat — периодическим ping-сообщением, которое не даёт соединению стать idle.
Heartbeat важнее большого таймаута
Полагаться только на длинный proxy_read_timeout — плохо: мёртвое соединение (клиент ушёл в туннель метро) будет считаться живым до часа. Лучше держать таймаут разумным и слать ping с клиента раз в 20-30 секунд, а на сервере отслеживать пропущенные ответы. Тогда сервер быстро узнаёт о разрыве и освобождает слот.
Устойчивость соединений и graceful reload
Вебсокеты меняют логику деплоя. При обычном рестарте WSGI-приложения запрос повторяется. С вебсокетом при перезапуске воркера все открытые соединения рвутся разом. Если у вас тысяча активных клиентов, релиз означает тысячу переподключений в одну секунду — и Redis, и новые воркеры получают всплеск.
Что с этим делать:
- Реконнект на клиенте обязателен. Клиентский код должен переподключаться с экспоненциальной задержкой и джиттером, а не мгновенно и синхронно — иначе тысяча клиентов ударит по серверу одновременно.
- Не хранить состояние только в памяти консьюмера. После реконнекта пользователь попадёт на другой воркер. Всё, что должно пережить разрыв, — в Redis или БД.
- Разделить деплой воркеров. Перезапускайте ASGI-воркеры порциями, а не все сразу, чтобы разрывы размазались по времени.
Аутентификация и лимиты
Вебсокет-хендшейк — это HTTP-запрос, к нему применяется AuthMiddlewareStack, если он в asgi.py. Но проверять права нужно и в самом консьюмере: middleware наполняет scope["user"], а решение принять или закрыть соединение — на вас.
- В
connect()явно проверяйтеscope["user"]и вызывайтеself.close()для анонимов, если ресурс закрытый. - Проверяйте
Origin— Channels 4.x умеет это черезOriginValidator/AllowedHostsOriginValidatorвasgi.py. Без этого сторонний сайт сможет открыть сокет от имени залогиненного пользователя. - Ограничивайте число соединений на пользователя и объём входящих сообщений — иначе один клиент откроет сотни сокетов или зальёт вас данными. Готового встроенного rate-limit в Channels нет, лимит считаете сами (например, через счётчик в Redis).
Мини-таблица: что проверить перед запуском
| Слой | Что проверить | Симптом при ошибке |
|---|---|---|
| Сервер | ASGI-сервер вместо runserver | Не держит нагрузку, падает под соединениями |
| Channel layer | Redis вместо InMemory | Рассылка доходит не до всех |
| Redis | capacity и expiry заданы осознанно | Рост памяти, OOM |
| Nginx | Upgrade-заголовки и proxy_read_timeout | Разрыв ровно через 60 секунд |
| Клиент | Реконнект с бэкоффом | Лавина переподключений при релизе |
| Auth | OriginValidator и проверка user | Чужой сайт открывает сокет от вашего пользователя |
Что мерить в проде
После запуска смотрите на три показателя. Первый — число одновременных вебсокет-соединений на воркер: он подскажет, когда пора добавлять процессы. Второй — память и число ключей в Redis: растущая память при стабильном числе клиентов означает, что expiry или capacity подобраны неверно и очереди мёртвых каналов не чистятся. Третий — частота реконнектов: всплески вне окон деплоя говорят о разрывах на прокси или нестабильной сети.
Следующий шаг после базового чек-листа — нагрузочный тест именно вебсокетов, а не HTTP. Инструмент вроде artillery с вебсокет-сценарием откроет вам, скажем, 2000 параллельных соединений и покажет, где связка ломается первой: в лимите файловых дескрипторов ОС, в числе воркеров или в Redis. Мерить это на живых пользователях — дорого.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Django Channels — официальная документация, channels_redis — репозиторий проекта
Частые вопросы
У меня Channels 3.x, а не 4.x — чек-лист подойдёт?
Обязательно ли Redis, или можно обойтись без channel layer?
Соединение рвётся ровно через 60 секунд молчания — где искать?
Что происходит с соединениями при деплое?
Как ограничить число соединений на одного пользователя?
Как понять, что дело в коде консьюмера, а не в инфраструктуре?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.