Building Scalable APIs with Django & Celery#Технологии
Окт 01, 2026 5 0
Поделиться

Building Scalable APIs with Django & Celery

Как выносить тяжёлую работу из запроса в очередь Celery: transaction.on_commit, идемпотентные задачи, лимиты API и контейнеры в production.

Otabek Xurramov
Python Backend-разработчик · CEO стартапа
3 мин чтения

Когда 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 станет быстрым и устойчивым.

BackendDjangoPython

Настройки звука

Эквалайзер
Скорость
Таймер сна

Клавиатура: Пробел — пуск/пауза, ← → — 10 секунд, Shift+← → — трек, M — без звука

Очередь