Не всё, что нужно сделать по запросу, должно случиться до ответа. Отправить письмо, пересчитать кэш, дёрнуть соседний сервис — это можно сделать после того, как клиент уже получил ответ. FastAPI даёт для этого BackgroundTasks, но у него жёсткие границы, и непонимание этих границ — частый источник потерянных данных.
BackgroundTasks: что это
BackgroundTasks — это список функций, которые FastAPI выполнит после отправки ответа, в том же процессе. Объявляется как параметр обработчика; задачи добавляются через add_task.
from fastapi import BackgroundTasks
@router.post("/", status_code=201)
async def create_product(
body: CreateProductRequest,
background_tasks: BackgroundTasks,
handler: CreateProductHandlerDep,
):
product = await handler.handle(body.to_command())
background_tasks.add_task(send_created_notification, product.id)
return ProductResponse.from_domain(product)
Клиент получает 201 сразу; уведомление уходит после. Для лёгких, необязательных действий «вдогонку» этого достаточно, и это сильно упрощает код.
Границы, которые надо знать
BackgroundTasks живёт внутри процесса приложения, и из этого следуют все его ограничения:
- Не переживает рестарт. Если процесс упал или его перезапустили после ответа, но до выполнения задачи, — задача потеряна, и никто об этом не узнает.
- Нет повторов. Упала задача — упала молча; механизма «попробовать ещё раз» нет.
- Делит ресурсы с обработкой запросов. Тяжёлая фоновая работа отъедает тот же процесс, что обслуживает запросы.
Поэтому правило: BackgroundTasks годится только для того, что не жалко потерять. Отправить необязательное уведомление — да. Списать деньги, провести заказ, гарантированно доставить событие — нет.
Когда нужна внешняя очередь
Как только работа важна (её нельзя терять), тяжела (долго считает) или должна повторяться при сбое — она выносится из процесса в отдельную систему: очередь задач с воркерами. В экосистеме Python это arq (асинхронная, на Redis) или Celery (зрелая, с брокером).
Задача кладётся в очередь, отдельный процесс-воркер её берёт, выполняет, при сбое повторяет. Приложение лишь ставит задачу и сразу отвечает; за надёжность отвечает очередь.
@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: «принято в обработку», а не «сделано». Это та же граница, что и для брокеров в большом сервисе: важное событие идёт через durable-механизм, а не «вдогонку» в памяти процесса.
Расписания нет — и это нормально
У FastAPI нет встроенного планировщика задач по времени, и это сознательно: веб-приложение, которое могут запустить в нескольких экземплярах, — плохое место для cron. Если запустить «по расписанию» внутри каждого экземпляра, задача выполнится столько раз, сколько экземпляров.
Регулярные задачи выносят наружу: системный cron, планировщик оркестратора (CronJob в Kubernetes), или тот же воркер очереди с поддержкой отложенных задач. Так расписание живёт в одном месте и не размножается с масштабированием приложения.
Где это в UCP
Главная мысль для методологии: фоновая работа — это отдельный путь, а не спрятанный хвост обработчика. У неё своя точка входа, свои гарантии, своя наблюдаемость. Прятать важную операцию в BackgroundTasks ради того, чтобы ответ был быстрым, — значит менять надёжность на скорость молча, и однажды это всплывёт потерянными данными.
Поэтому выбор прост и его стоит делать осознанно: необязательное «вдогонку» — BackgroundTasks; важное, тяжёлое или повторяемое — внешняя очередь. Этот выбор между «в памяти процесса» и «в durable-системе» — то же инфраструктурное решение, что и при работе с брокерами в Spring-биндинге, и принимает его продукт-инженер, а не оставляет на «как получилось». Долгие синхронные вызовы внутри обработчика — отдельная тема конкурентности.