Когда API начинает тормозить, первая мысль — усилить сервер. Но чаще проблема в другом: внутри запроса выполняется работа, которой там не место. Письма, сообщения в Telegram, обработка изображений, обращения к внешним API — ничто из этого не стоит того, чтобы пользователь ждал ответа. В статье покажу, как я выношу такие задачи в фон с Django и Celery, на примерах из WeliCoDev и NutriZone.
1. Правило: запрос — только для ответа
Внутри HTTP-запроса остаётся только то, что нужно для ответа: проверка, запись в базу, ответ. Всё остальное — в очередь.
- Django принимает запрос и ставит задачу в очередь;
- Redis — брокер;
- Celery worker выполняет задачи в фоне;
- Celery beat — задачи по расписанию.
2. Частая ошибка: задача до коммита транзакции
Если вызвать task.delay(obj.pk) сразу после сохранения, worker может не найти объект — транзакция ещё не закоммичена. Решение — transaction.on_commit:
from django.db import transaction
def on_article_published(instance):
transaction.on_commit(
lambda: post_to_channel.delay("blog.Article", instance.pk)
)
3. Идемпотентность
Celery может повторить задачу (retry), worker может перезапуститься. Каждая задача должна спокойно переживать двойной запуск. Для постов в канал у меня две защиты: метка на объекте и таблица отправленных постов.
@shared_task
def post_to_channel(model_label, pk):
obj = Model.objects.filter(pk=pk).first()
if obj is None or obj.posted_to_channel_at:
return
if ChannelPost.objects.filter(object_id=pk, status="sent").exists():
return
post = channel.post_object(obj)
if post.status == "sent":
Model.objects.filter(pk=pk).update(posted_to_channel_at=timezone.now())
В задачу передаётся только pk — актуальное состояние читается из базы в момент выполнения.
4. Лимиты внешних API и повторы
Telegram позволяет боту около 25 сообщений в секунду. Для рассылок: ограничивайте скорость, при временных ошибках используйте retry, при постоянных (403 — пользователь заблокировал бота) — прекращайте и фиксируйте это в базе.
5. Расписание: Celery beat
CELERY_BEAT_SCHEDULE = {
"send-due-telegram-posts": {
"task": "apps.telegram_bot.tasks.send_due",
"schedule": 60.0,
},
}
Не забудьте выставить CELERY_TIMEZONE (у нас Asia/Tashkent).
6. Production: отдельные контейнеры
Web, worker и beat — один образ, разные сервисы. Worker масштабируется независимо, beat всегда в одном экземпляре — иначе задачи уйдут дважды.
7. Чек-лист
- В задачу — только ID.
- После сохранения —
transaction.on_commit. - Идемпотентные задачи.
- Отдельная обработка 4xx и 5xx.
- Один экземпляр beat.
- В тестах — mock внешних API и проверка двойного вызова.
Итог
Держите запрос лёгким, тяжёлую работу отдавайте в очередь и пишите задачи с расчётом на повтор — API станет быстрым и устойчивым.