← назад к разделу

PYTHON · FASTAPI Фаза 5. Фундамент - ядро FastAPI

Обязательно

Фоновые задачи и расписание в FastAPI

BackgroundTasks · Queue · Outbox

Быстрый ответ не гарантирует выполнение работы: встроенные задачи живут в памяти, надёжная обработка требует очереди.

1. BackgroundTasks выполняется после ответа

1 / 5

Обработчик создаёт пользователя, добавляет отправку письма и возвращает 201. Функция выполняется после отправки ответа в том же процессе.

create_useradd_task(email)201 Createdresponse sentsend_emailsame process

2. Память процесса не гарантирует работу

2 / 5

При рестарте задача исчезает, при ошибке нет автоматического повтора, тяжёлая работа конкурирует с запросами. Подходит только то, что не жалко потерять.

Restartin-memory taskпотерянаErrorнет retryтолько логHeavy jobCPU + memoryзапросы страдают

3. Важная работа идёт в очередь

3 / 5

Приложение возвращает 202 и job_id, отдельный worker выполняет задачу из Redis или брокера. После сбоя задание остаётся и может быть повторено.

FastAPI202 + job_idRedis / Brokerdurable jobretry after failureWorkerarq / Celery

4. Планировщик не дублируется в подах

4 / 5

FastAPI не запускает расписание: четыре экземпляра выполнили бы задачу четыре раза. Простые задания идут в cron или Kubernetes CronJob, очередные - в планировщик worker.

Pod 1Pod 2CronJobодно задание по времениWorker

5. At least once требует идемпотентности

5 / 5

Worker может повторить частично выполненную задачу. Ключ не даёт отправить письмо или списать деньги дважды, а outbox записывает задание в одной транзакции с данными.

Transactionorder + outbox eventcommit вместеWorker × 2 attemptsidempotency_keyeffect выполнен один раз

Иногда запрос надо обработать быстро, но часть работы можно отложить. Отправить письмо, обновить кэш, уведомить другой сервис — всё это не обязательно делать до того, как клиент получит ответ. FastAPI даёт для этого инструмент BackgroundTasks. Но у него есть жёсткие ограничения, и если их не знать, можно потерять данные.

Зачем откладывать работу на после ответа

Представьте: пользователь регистрируется, и ему надо отправить письмо с подтверждением. Если отправлять письмо синхронно прямо в обработчике, пользователь ждёт. Почтовый сервер может тормозить, сеть — барахлить. Зачем держать клиента, если он уже зарегистрирован?

Ответ можно отправить сразу, а письмо — чуть позже. Это и есть фоновая задача.

BackgroundTasks: как это работает

BackgroundTasks — объект из стандартной библиотеки FastAPI. Он хранит список функций и запускает их после того, как ответ отправлен клиенту. Подключается как параметр обработчика — FastAPI создаёт его автоматически.

from fastapi import BackgroundTasks, APIRouter

router = APIRouter()


@router.post("/register", status_code=201)
async def register_user(
    body: RegisterRequest,
    background_tasks: BackgroundTasks,
):
    user = await create_user(body)
    background_tasks.add_task(send_welcome_email, user.email)
    return {"id": user.id}

Клиент получает 201 сразу, как только пользователь создан. Письмо уходит после — в том же процессе, в той же памяти.

Функция в add_task может быть как обычной (def), так и асинхронной (async def) — FastAPI разберётся.

Где BackgroundTasks работает, а где нет

Главное, что надо понять: BackgroundTasks живёт внутри процесса. Никакой базы, никакой очереди — просто список функций в памяти. Отсюда все ограничения:

BackgroundTasks ответ 201 клиенту функция в памяти процесса рестарт: задача исчезла очередь задач ответ 202 Accepted запись в Redis или брокер воркер повторит после сбоя

BackgroundTasks живёт в памяти процесса и не переживает перезапуск; важная работа идёт через очередь с отдельным воркером.

Не переживает остановку приложения. Процесс перезапустили, упал контейнер, развернули новую версию — задача в памяти исчезла. Никто об этом не узнает, никакого журнала нет.

Нет повторных попыток. Если функция упала с ошибкой — ошибка попадёт в лог (или не попадёт, если не настроено), но задача не повторится автоматически.

Делит ресурсы с обработкой запросов. Если фоновая задача тяжёлая — она конкурирует за CPU и память с запросами пользователей.

Из этого следует простое правило:

BackgroundTasks годится только для того, что не жалко потерять.

Отправить необязательное уведомление — подходит. Провести платёж, подтвердить заказ, гарантированно что-то доставить — нет.

Когда нужна внешняя очередь задач

Как только задача становится важной, тяжёлой или должна повторяться при сбое — её выносят из процесса в отдельную систему. В мире Python для этого есть два основных инструмента:

  • arq — асинхронная очередь задач на базе Redis. Хорошо вписывается в async-стек FastAPI.
  • Celery — зрелый, функциональный, поддерживает Redis и RabbitMQ как брокер.

Принцип работы одинаковый: приложение кладёт задачу в очередь (запись в Redis или брокер), отдельный процесс-воркер её забирает и выполняет. Если воркер упал — задача остаётся в очереди и будет обработана при следующей попытке.

@router.post("/import", status_code=202)
async def import_catalog(body: ImportRequest, queue: QueueDep):
    job = await queue.enqueue_job("process_import", body.file_id)
    return {"job_id": job.job_id}

Код ответа 202 Accepted здесь точнее, чем 200: «задача принята», а не «задача выполнена». Это честная коммуникация с клиентом — он знает, что результат будет позже.

Расписание: почему его нет в FastAPI

FastAPI не предоставляет встроенного планировщика по времени — и это осознанное решение.

Веб-приложение обычно запускают в нескольких экземплярах, чтобы выдержать нагрузку. Если у каждого экземпляра есть свой планировщик, одна и та же задача «по расписанию» выполнится столько раз, сколько экземпляров запущено. Это почти всегда не то, чего хотят.

Регулярные задачи выносят наружу:

  • Системный cron — если задача простая и запускается раз в сутки.
  • CronJob в Kubernetes — если приложение работает в контейнерах; задача запускается отдельным контейнером по расписанию.
  • Планировщик в очереди задач — arq и Celery умеют запускать задачи по расписанию через воркер; расписание живёт в одном месте и не зависит от числа экземпляров приложения.
Дополнительно: при первом чтении можно пропустить

Глубже: create_task, повторы и идемпотентностьрасширенное

Соблазн обойтись без BackgroundTasks и написать asyncio.create_task(send_email(...)) прямо в обработчике. У этого приёма две беды. Задачу, на которую никто не хранит ссылку, сборщик мусора может убрать прямо посреди работы, поэтому ссылки складывают в множество и убирают по завершении. И никто не дожидается таких задач при остановке: сервис получил SIGTERM, event loop закрылся, письмо не ушло. BackgroundTasks хотя бы привязан к запросу, и сервер даёт ему доработать.

Очередь задач тоже не волшебная: воркер забрал задачу, выполнил половину и упал — при перезапуске задача придёт снова. Доставка «хотя бы раз» означает, что обработчик обязан быть идемпотентным: письмо с тем же ключом не отправлять дважды, заказ с тем же номером не подтверждать повторно. Если задача рождается вместе с записью в базу, надёжнее всего положить её в ту же транзакцию — это шаблон outbox из статьи о распределённых паттернах.

Из очередей для async-стека, кроме arq, стоит знать taskiq: он умеет те же зависимости через Depends, что и FastAPI, и запускает задачи в event loop воркера. А про сессию базы: с FastAPI 0.118 очистка yield-зависимостей идёт после фоновых задач, так что сессия в BackgroundTasks ещё жива; в более старых версиях она к этому моменту уже закрыта, и в задачу передают идентификаторы.

Коротко

  • BackgroundTasks запускает функции после отправки ответа — в том же процессе, в памяти.
  • Задача в BackgroundTasks теряется при рестарте процесса; повторных попыток нет.
  • Используйте BackgroundTasks только для необязательной работы — той, потерю которой можно пережить.
  • Для важного, тяжёлого или повторяемого — arq или Celery с отдельным воркером и Redis-брокером.
  • Статус 202 Accepted честнее 200, когда работа только поставлена в очередь.
  • Встроенного планировщика в FastAPI нет намеренно; расписание выносят в cron, CronJob или в планировщик очереди задач.
  • asyncio.create_task без ссылки и без ожидания при остановке теряет работу; очередь доставляет «хотя бы раз», поэтому обработчик идемпотентен, а важные задачи кладут в транзакцию (outbox).

Что почитать дальше