Фоновые задачи в FastAPI: рабочие приёмы и типичные ошибки
BackgroundTasks в FastAPI выглядят как простой способ отложить работу после ответа клиенту. На практике задачи теряются при перезапуске, блокируют event loop и молча падают без логов. Разбираем, как использовать встроенный механизм правильно и когда пора переходить на Celery или arq.

Зачем вообще фоновые задачи
Клиент отправил заказ — ему не нужно ждать, пока сервер сгенерирует PDF-счёт и отправит письмо. Ответ 200 можно отдать сразу, а тяжёлую работу выполнить после. FastAPI (версии 0.100+, Starlette 0.27+) предлагает для этого класс BackgroundTasks: вы добавляете callable, и фреймворк выполняет его после отправки ответа — в том же процессе, в том же event loop. Это удобно, пока вы не столкнётесь с тремя вещами: потерей задач, блокировкой цикла и отсутствием повторных попыток.
Как BackgroundTasks работает изнутри
Starlette хранит список задач в объекте BackgroundTasks, привязанном к конкретному Response. После того как ASGI-сервер (Uvicorn, Hypercorn) отправил тело ответа клиенту, он вызывает каждую задачу последовательно. Если задача — корутина, она выполняется через await. Если обычная функция — Starlette оборачивает её в run_in_threadpool, то есть запускает в пуле потоков anyio.
Ключевой момент: задачи живут в памяти процесса. Если Uvicorn перезапустится (деплой, OOM-kill, сигнал), задачи, которые ещё не начали выполняться, исчезнут. Никакого retry, никакого журнала — просто тишина.
Встроенные фоновые задачи FastAPI — это «fire and forget» в буквальном смысле. Они подходят для работы, потерю которой вы готовы пережить: отправка метрики, запись в лог, обновление кеша. Для всего, что нельзя потерять, нужна очередь с персистентностью.
Правильное использование: четыре приёма
1. Отделяйте sync от async
Если задача делает CPU-bound работу (генерация изображения, сжатие), она должна быть обычной функцией, а не корутиной. Starlette отправит её в поток, и event loop продолжит обслуживать другие запросы. Если же вы пометите такую функцию как async def, она заблокирует цикл до завершения — остальные клиенты будут ждать.
# FastAPI 0.100+, Python 3.11+
from fastapi import FastAPI, BackgroundTasks
import time
app = FastAPI()
def generate_report(order_id: int) -> None:
"""Синхронная задача — выполнится в потоке, не заблокирует loop."""
time.sleep(5) # имитация тяжёлой работы
# сохраняем отчёт на диск или в S3
@app.post("/orders/{order_id}/report")
async def request_report(order_id: int, bg: BackgroundTasks):
bg.add_task(generate_report, order_id)
return {"status": "accepted", "order_id": order_id}
Проверка: во время выполнения generate_report другие эндпоинты должны отвечать без задержки. Если нет — вы случайно написали async def generate_report.
2. Ловите исключения внутри задачи
Starlette не оборачивает задачу в try/except за вас. Необработанное исключение попадёт в логи Uvicorn как ERROR: Exception in ASGI application, но что именно упало — придётся угадывать. Оборачивайте тело задачи явно:
import logging
logger = logging.getLogger("bg_tasks")
def send_email(to: str, subject: str, body: str) -> None:
try:
# вызов SMTP / API почтового сервиса
...
except Exception:
logger.exception("Не удалось отправить письмо на %s", to)
# здесь можно записать задачу в таблицу failed_tasks для ручного retry
3. Не передавайте ORM-сессию в задачу
Частая ошибка: эндпоинт получает db: Session через Depends и передаёт её в фоновую задачу. К моменту выполнения задачи сессия уже закрыта — вы получите DetachedInstanceError или SessionClosed. Создавайте новую сессию внутри задачи:
from app.database import SessionLocal
def update_stock(product_id: int, delta: int) -> None:
db = SessionLocal()
try:
product = db.query(Product).filter_by(id=product_id).first()
if product:
product.stock += delta
db.commit()
finally:
db.close()
4. Ограничивайте количество задач на запрос
Ничто не мешает клиенту вызвать эндпоинт тысячу раз в секунду — и каждый раз положить в очередь задачу. Пул потоков anyio по умолчанию ограничен 40 потоками. Когда все заняты, новые задачи встают в очередь и ждут, раздувая потребление памяти. Используйте rate limiting на уровне middleware или reverse-proxy.
Когда встроенных задач недостаточно
Три сигнала, что пора переезжать на внешнюю очередь:
- Задачу нельзя потерять — отправка транзакционного письма, начисление баллов, вебхук партнёру.
- Задача выполняется дольше 30 секунд — генерация видео, импорт каталога. Процесс Uvicorn может быть перезапущен раньше.
- Нужны повторные попытки с экспоненциальной задержкой — встроенный механизм этого не умеет вовсе.
Сравнение подходов
| Критерий | BackgroundTasks | Celery + Redis/RabbitMQ | arq (asyncio) |
|---|---|---|---|
| Персистентность | Нет, только в памяти | Да, брокер хранит сообщения | Да, Redis |
| Retry | Нет | Да, гибкая настройка | Да, встроенный |
| Мониторинг | Только логи | Flower, Prometheus-экспортёры | Встроенный dashboard |
| Инфраструктура | Ничего — всё в процессе | Redis или RabbitMQ + worker-процесс | Redis + worker-процесс |
| Сложность внедрения | Одна строка | Высокая: настройка брокера, сериализация, деплой воркеров | Средняя |
| Подходит для | Лёгких задач, потеря которых допустима | Продакшн-очередей любого масштаба | Проектов на asyncio без тяжёлой инфраструктуры |
Типичные ошибки и как их диагностировать
Ниже — пять ситуаций, которые регулярно всплывают в проектах на FastAPI.
- Задача блокирует event loop. Симптом: во время выполнения фоновой задачи остальные эндпоинты отвечают с задержкой или таймаутом. Причина: задача объявлена как
async def, но внутри вызывает синхронный код безrun_in_executor. Исправление: уберитеasync, оставьте обычнуюdef— Starlette сама отправит её в поток. - DetachedInstanceError из SQLAlchemy. Симптом: задача падает при обращении к атрибуту модели. Причина: ORM-сессия передана из dependency и уже закрыта. Исправление: создавайте сессию внутри задачи (см. приём 3 выше).
- Задачи «пропадают» после деплоя. Симптом: письма не доходят, отчёты не генерируются, логов ошибок нет. Причина: Uvicorn получил SIGTERM, задачи в очереди не дождались выполнения. Исправление: для критичных задач — очередь с персистентностью; для остальных — хотя бы логирование момента постановки в очередь, чтобы было что расследовать.
- Утечка памяти при массовой постановке. Симптом: RSS процесса растёт при высокой нагрузке. Причина: каждая задача держит аргументы в памяти до выполнения; при тысячах запросов в секунду очередь раздувается. Исправление: rate limiting, переход на внешнюю очередь.
- Необработанное исключение убивает цепочку. Симптом: первая задача падает, вторая не выполняется. Причина: Starlette выполняет задачи последовательно; если одна бросает исключение, остальные не запускаются. Исправление: оборачивайте каждую задачу в try/except.
* event loop — цикл обработки событий в asyncio. Один поток обслуживает все подключения; если его заблокировать синхронной операцией, весь сервер встанет.
Когда что выбирать
Решение зависит от двух вопросов: «можно ли потерять задачу?» и «сколько она длится?».
- BackgroundTasks — задача длится до нескольких секунд, потеря допустима. Примеры: запись аудит-лога в отдельную таблицу, инвалидация кеша, отправка аналитического события.
- arq — задача длится до минут, потеря нежелательна, проект целиком на asyncio и нет желания тянуть RabbitMQ. Примеры: отправка email, ресайз изображений.
- Celery — задача длится минуты-часы, потеря недопустима, нужны цепочки, расписания, мониторинг. Примеры: импорт каталога, генерация отчётов, интеграция с внешними API с retry.
Что делать дальше
Если вы сейчас используете BackgroundTasks для критичной логики — проведите аудит: составьте список всех вызовов add_task, пометьте каждый как «потеря допустима» или «потеря недопустима». Вторую группу переносите в arq или Celery. Для оставшихся задач добавьте обработку исключений и логирование — без этого вы узнаете о проблеме только от пользователя, который не получил письмо.
Подробности о жизненном цикле фоновых задач — в документации Starlette, раздел Background Tasks. Поведение run_in_threadpool и размер пула потоков зависят от версии anyio, которую подтянет ваша версия Starlette: проверяйте pip show anyio.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: Starlette — Background Tasks (официальная документация), FastAPI — Background Tasks (официальная документация)
Частые вопросы
BackgroundTasks выполняются параллельно или последовательно?
Можно ли передать в фоновую задачу сессию SQLAlchemy из dependency?
Что будет с задачами, если Uvicorn получит SIGTERM?
Как понять, что фоновая задача блокирует event loop?
Есть ли ограничение на количество фоновых задач в одном запросе?
Можно ли использовать BackgroundTasks вместе с Celery в одном проекте?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.