Async & Background Tasks

FastAPI поддерживает и def, и async def эндпоинты. Разница важна.

sync vs async

Тип Когда использовать
def CPU-bound или I/O-bound через синхронные либы (psycopg2, requests)
async def I/O через async-либы (httpx, asyncpg, redis.asyncio)

Микс опасен: если внутри async def вызвать блокирующий вызов — фреймворк не отрубит его, и весь event loop встанет.

# ПЛОХО — блокирует event loop
@app.get("/slow")
async def slow():
    requests.get("https://example.com")  # синхронный I/O!
    return {}

# ХОРОШО
@app.get("/slow")
async def slow():
    async with httpx.AsyncClient() as client:
        await client.get("https://example.com")
    return {}

BackgroundTasks

Запустить что-то после ответа клиенту (без ожидания):

from fastapi import BackgroundTasks

def send_email(to: str):
    smtp.send(to)  # блокирующий I/O — ОК, выполняется после ответа

@app.post("/signup")
async def signup(bg: BackgroundTasks, email: str):
    bg.add_task(send_email, email)
    return {"status": "ok"}

Подходит для: email, метрики, логирование. Не подходит для задач, которые должны пережить рестарт приложения.

Lifespan

Одноразовая инициализация/очистка ресурсов на старте/остановке приложения:

from contextlib import asynccontextmanager

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.pool = await asyncpg.create_pool(DSN)
    yield
    await app.state.pool.close()

app = FastAPI(lifespan=lifespan)

Пул, redis-клиент, kafka-продюсер — всё что должно жить весь процесс.

Когда что

  • Один запрос, без побочек → async def
  • Фоновая работа после ответа → BackgroundTasks
  • Долгоживущие воркеры → отдельный процесс/очередь (Celery, ARQ)
  • Глобальный ресурс → lifespan + app.state

Грабли

  • asyncio.create_task(...) без ссылки — может быть собран GC до завершения
  • BackgroundTasks крутятся в том же процессе; падение приложения их убивает
  • Логирование внутри async def через print блокирует event loop

Связанные заметки