Не всё, что нужно сделать по запросу, должно случиться до ответа. Отправить письмо, пересчитать кэш, дёрнуть соседний сервис — это можно сделать после того, как клиент уже получил ответ. 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-биндинге, и принимает его продукт-инженер, а не оставляет на «как получилось». Долгие синхронные вызовы внутри обработчика — отдельная тема конкурентности.