Короткий ответ
Разница важна, когда внутри обработчика выполняется блокирующий I/O (синхронный HTTP-клиент, синхронный драйвер БД, time.sleep): такой код в async def-эндпоинте блокирует единственный event loop и замедляет обработку всех остальных параллельных запросов.
Как это работает подробнее
async def-эндпоинты выполняются в event loop; любой синхронный блокирующий вызов внутри них «замораживает» этот loop на время своего выполнения, не давая обслуживать другие корутины. Обычные def-эндпоинты FastAPI автоматически выполняются в отдельном thread pool (через Starlette), поэтому блокирующий код там не останавливает event loop целиком.
Пример кода
example.py
import time
from fastapi import FastAPI
app = FastAPI()
# Плохо: блокирует event loop на 2 секунды для ВСЕХ запросов
@app.get("/bad")
async def bad_endpoint():
time.sleep(2) # синхronный блокирующий вызов внутри async def
return {"ok": True}
# Лучше в этом случае - обычный def, FastAPI выполнит его в threadpool
@app.get("/good")
def good_endpoint():
time.sleep(2)
return {"ok": True}
# Нагрузочный тест параллельными запросами к /bad покажет сериализацию
# по времени (~2с * N), а к /good - параллельное выполнение в разных потоках.Что использовать на практике
- Практика. Если библиотека для похода в БД/внешний сервис не умеет
async/await(синхронный драйвер), для такого эндпоинта логичнее использовать обычныйdef— тогда FastAPI сам унесёт вызов в поток. Если же используется асинхронный драйвер (например,asyncpg,httpx.AsyncClient), стоит писатьasync defи действительно использоватьawait.
Подводные камни
- Риск. Пишут
async def«по умолчанию», но внутри вызывают синхронную библиотеку безrun_in_executor/run_in_threadpool, получая деградацию производительности под нагрузкой именно из-за блокировки event loop, а не ускорение; путают факт параллельного выполнения корутин с параллелизмом CPU-bound вычислений, которогоasyncioне даёт.