Иногда запрос надо обработать быстро, но часть работы можно отложить. Отправить письмо, обновить кэш, уведомить другой сервис — всё это не обязательно делать до того, как клиент получит ответ. 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 живёт в памяти процесса и не переживает перезапуск; важная работа идёт через очередь с отдельным воркером.
Не переживает остановку приложения. Процесс перезапустили, упал контейнер, развернули новую версию — задача в памяти исчезла. Никто об этом не узнает, никакого журнала нет.
Нет повторных попыток. Если функция упала с ошибкой — ошибка попадёт в лог (или не попадёт, если не настроено), но задача не повторится автоматически.
Делит ресурсы с обработкой запросов. Если фоновая задача тяжёлая — она конкурирует за 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).
Что почитать дальше
- Конкурентность и async в FastAPI — как устроена асинхронная обработка запросов.
- Наблюдаемость FastAPI — как следить за фоновыми задачами через логи и метрики.