Нагрузка в FastAPI: цифры, узкие места и выводы
FastAPI часто называют «самым быстрым Python-фреймворком», но реальные цифры под нагрузкой зависят не от фреймворка, а от того, что делает ваш код между получением запроса и отправкой ответа. Разбираем, где FastAPI действительно быстр, где упирается в потолок и как это измерить на своём проекте.

Почему бенчмарки из README не работают в проде
Тесты из репозитория TechEmpower и маркетинговых статей измеряют одно: скорость пустого JSON-ответа. В реальном приложении между входом запроса и выходом ответа лежат валидация Pydantic, поход в базу, сериализация, иногда — синхронный код в def-эндпоинтах, который FastAPI оборачивает в пул потоков. Каждый из этих слоёв добавляет латентность и снижает пропускную способность, причём вклад каждого слоя различается на порядок в зависимости от сценария.
Статья описывает поведение FastAPI 0.110+ на Uvicorn (CPython 3.11–3.12). Если вы используете Granian или Hypercorn — результаты будут другими, хотя общие узкие места останутся теми же.
Что и как мерить
Прежде чем говорить о цифрах, нужно договориться о методике. Без неё любые числа — шум.
Инструменты
- wrk или wrk2 — для замера пропускной способности (requests per second, RPS) и распределения латентности. wrk2 точнее фиксирует хвосты (p99), потому что поддерживает постоянную целевую нагрузку.
- py-spy — для профилирования CPU без инструментирования кода. Показывает, где именно тратится время внутри Python-процесса.
- yappi — если нужна разбивка по корутинам, а не только по потокам.
- pgbench или аналог — чтобы изолировать время базы и не списывать задержку PostgreSQL на фреймворк.
Условия замера
Минимально фиксируйте: количество воркеров Uvicorn, количество соединений и потоков в wrk, объём данных в ответе, наличие и глубину Pydantic-модели, тип обращения к БД (sync/async), объём пула соединений. Без этих параметров числа невоспроизводимы.
Главная ошибка — мерить всё вместе. Пока вы не знаете, сколько занимает база, сколько — сериализация, а сколько — сам ASGI-стек, оптимизировать нечего: вы стреляете в темноту.
Где FastAPI быстр, а где нет: слой за слоем
ASGI-стек: маршрутизация и middleware
Собственно маршрутизация в Starlette (на которой построен FastAPI) — операция порядка десятков микросекунд. На пустом эндпоинте с одним Uvicorn-воркером на типичном серверном CPU (Xeon, Ryzen) можно получить 15–30 тысяч RPS. Это потолок ASGI-стека в CPython — и он редко бывает узким местом.
Middleware снижает этот потолок линейно: каждый слой добавляет пару вызовов корутин. Три-четыре middleware — терпимо. Десять — уже заметно на p99 при высокой конкурентности.
Pydantic-валидация
Pydantic v2 (на ядре pydantic-core, написанном на Rust) работает в 5–15 раз быстрее Pydantic v1 при валидации вложенных моделей. Но «быстрее» не значит «бесплатно». На модели с двумя-тремя полями разницу не заметить. На модели из 40 полей с вложенными списками объектов — десятки микросекунд на запрос, и при 5 000 RPS это уже ощутимая доля CPU.
Частая ошибка: response_model с тяжёлой моделью на эндпоинте, который возвращает список из сотен объектов. Сериализация ответа через Pydantic в таком сценарии может занимать больше времени, чем сам запрос к базе.
Синхронный код и пул потоков
Если эндпоинт объявлен через def, а не async def, FastAPI исполняет его в anyio-пуле потоков (по умолчанию — до 40 потоков). Каждый такой вызов — это переключение контекста и ожидание свободного потока. Под нагрузкой пул становится узким горлом: запросы копятся в очереди, латентность растёт нелинейно, а p99 улетает.
Правило простое: если внутри эндпоинта только await-вызовы — объявляйте async def. Если вызываете синхронную библиотеку (например, requests, синхронный ORM) — либо замените её на асинхронный аналог, либо явно контролируйте размер пула.
Обращение к базе данных
Чаще всего именно база определяет латентность ответа. Разница между sync-драйвером через пул потоков и async-драйвером (asyncpg, databases) — не в «скорости запроса», а в том, как используется event loop во время ожидания ответа от PostgreSQL. Асинхронный драйвер не блокирует воркер — и один процесс обслуживает больше одновременных соединений.
| Слой | Типичный вклад в латентность (один запрос) | Когда становится узким местом |
|---|---|---|
| ASGI-маршрутизация + middleware | 50–200 мкс | Больше 8–10 middleware, высокая конкурентность |
| Pydantic-валидация (v2, модель ~10 полей) | 10–50 мкс | Тяжёлая response_model с сотнями объектов |
| Пул потоков (def-эндпоинт) | 100–500 мкс ожидания в очереди | Все эндпоинты — sync def, нагрузка > 500 RPS |
| Запрос к PostgreSQL (async) | 1–50 мс | Медленные запросы, маленький connection pool |
| Сериализация JSON (stdlib json) | 50–300 мкс на средний ответ | Большие ответы (>100 КБ), отсутствие orjson |
Цифры — порядок величин на типичном серверном железе, не абсолютные значения. Мерьте на своём стенде.
Три вещи, которые ломаются первыми
1. Connection pool к базе
По умолчанию SQLAlchemy async и asyncpg создают пул на 5–10 соединений. Если Uvicorn запущен с четырьмя воркерами, каждый получает свой пул — итого 20–40 соединений. PostgreSQL по умолчанию принимает 100. Добавьте сюда фоновые задачи, миграции и мониторинг — и вы упираетесь в max_connections раньше, чем в CPU. Используйте PgBouncer или контролируйте размер пула явно.
2. Сериализация больших ответов
Стандартный json.dumps в CPython — медленный. Замена на orjson (через кастомный ORJSONResponse из самого FastAPI или вручную) даёт ускорение сериализации в 2–6 раз на реальных структурах. На эндпоинтах, отдающих список из тысяч объектов, это разница между 4 мс и 20 мс на ответ.
3. Блокирующий код в async def
Обратная ситуация: эндпоинт объявлен как async def, но внутри вызывается синхронная операция — чтение файла без aiofiles, HTTP-запрос через requests, CPU-тяжёлый расчёт. Event loop блокируется, и все остальные запросы в этом воркере встают в очередь. Один такой эндпоинт способен уронить p99 всего сервиса.
Диагностика: включите в Uvicorn логирование медленных корутин или используйте loop.slow_callback_duration для детекции блокировок event loop длиннее заданного порога.
Масштабирование: воркеры, процессы, реплики
Uvicorn — однопроцессный ASGI-сервер. Для утилизации нескольких ядер запускают несколько воркеров (через флаг --workers или через Gunicorn с uvicorn.workers.UvicornWorker). Рекомендация «2× CPU + 1» — отправная точка, но не закон: при IO-bound нагрузке больше воркеров может быть лучше, при CPU-bound — хуже (из-за конкуренции за GIL внутри одного процесса, хотя каждый воркер — отдельный процесс и GIL у него свой).
Ключевые вопросы при масштабировании:
- Сколько оперативной памяти занимает один воркер при прогретых кешах и загруженных моделях? Умножьте на количество воркеров — это ваш базовый footprint.
- Как масштабируется connection pool к базе? Каждый воркер создаёт свой пул.
- Есть ли in-process состояние (кеш, ML-модель в памяти), которое дублируется при увеличении воркеров?
В Kubernetes чаще масштабируют горизонтально — один воркер на контейнер, реплик много. Это проще для оркестрации и health check-ов, но дороже по памяти.
Что мерить в проде
Бенчмарк на пустом стенде — не прод. В проде нужны метрики, которые покажут деградацию до того, как пользователи начнут жаловаться.
- p50 / p95 / p99 латентность по эндпоинтам. Средняя латентность бесполезна — она скрывает хвосты.
- Количество активных соединений к базе vs. размер пула. Если значения сходятся — пул на пределе.
- Длина очереди пула потоков (если используете sync-эндпоинты). Растёт — значит, запросы ждут.
- Event loop lag — время между запланированным и фактическим выполнением callback. Рост означает блокировку loop.
- Время GC — на больших объёмах объектов в памяти сборка мусора CPython может вносить паузы.
Для экспорта метрик удобны prometheus-fastapi-instrumentator или ручной middleware с Prometheus client. Оба варианта добавляют незначительный overhead — порядка единиц микросекунд на запрос.
Что делать дальше
Измерьте свой сервис послойно: отдельно ASGI-стек (пустой эндпоинт), отдельно — с Pydantic-моделью, отдельно — с реальным запросом к базе. Сравните цифры — и вы увидите, какой слой доминирует. Оптимизируйте именно его, а не «FastAPI вообще».
Следующий шаг — настройка мониторинга p99 в проде и алертов на рост event loop lag. Без этих двух метрик вы узнаете о деградации от пользователей, а не от графиков.
Для глубокого погружения в ASGI-стек и поведение Uvicorn под нагрузкой — читайте раздел Deployment в официальной документации FastAPI и документацию Uvicorn по настройке воркеров и таймаутов.
ASGI (Asynchronous Server Gateway Interface) — спецификация взаимодействия между Python-приложением и веб-сервером, поддерживающая асинхронную обработку запросов, WebSocket и HTTP/2. Пришла на смену WSGI, которая работает только синхронно.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: FastAPI — официальная документация (Deployment), Uvicorn — официальная документация (Settings)
Частые вопросы
FastAPI действительно быстрее Flask и Django?
Сколько RPS можно получить от FastAPI на одном ядре?
Нужен ли Gunicorn, если есть Uvicorn?
Что будет, если не перейти с def на async def?
orjson действительно даёт заметный прирост?
Как понять, что event loop заблокирован?
Что если у меня другая версия Python или Uvicorn — цифры применимы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.