«Давайте перепишем на Go, Python не тянет» — предложение, которое звучит на каждом втором разборе тормозящего API. Иногда оно верное, но гораздо чаще за ним стоит отсутствующий индекс в Postgres или сериализация, которую никто не смотрел. Ниже — как отличить одно от другого: где Go и Rust действительно дают выигрыш при масштабировании Python-приложений, какой стек 2026 года брать, сколько миграция стоит в деньгах и почему в большинстве случаев правильный ответ — переписать один эндпоинт, а не сервис.
Почему Python-бэкенд работает медленно: профилирование под нагрузкой
В типичном веб-API язык почти никогда не главный источник задержки. Разложите время ответа на составляющие — обычно картина такая: 60–80% времени сервис ждёт базу, 10–20% уходит на сеть и внешние вызовы, и лишь остаток — собственно исполнение вашего кода. Переписывание на Rust ускоряет только этот остаток. Если эндпоинт отвечает за 400 мс, из которых 340 мс — запрос без индекса, переход на Rust даст вам 395 мс вместо 400. Неловко получилось.
Чтобы найти реальные узкие места производительности Python, нужно измерить пять вещей до любого разговора о языке:
-
Разбивка времени ответа по эндпоинту: сколько в базе, сколько в приложении, сколько во внешних HTTP-вызовах. Подойдёт любой APM или ручная трассировка через OpenTelemetry.
-
Топ эндпоинтов по суммарному CPU, а не по количеству запросов. Часто 2% трафика дают 70% нагрузки на процессор.
-
Флеймграф процесса под реальной нагрузкой. Профилирование Python в проде удобнее всего делать через
py-spy: он цепляется к живому процессу без перезапуска и сразу показывает, где сгорает время — в вашем цикле, в сериализации или в ORM.py-spy record -o profile.svg --pid 1 --duration 60 py-spy dump --pid 1 # что прямо сейчас делает каждый поток -
Сколько запросов делает эндпоинт. Классический N+1 в SQLAlchemy — это 200 запросов вместо одного, и никакой язык это не лечит.
-
Профиль памяти. Если Python-бэкенд ест много памяти, смотрите не на общий RSS, а на пик на один запрос:
memrayпокажет, что 900 МБ съедает выгрузка, которая читает весь результат в список вместо курсора. Это чинится потоковой отдачей, а не сменой языка.
Отдельный частый случай — аналитические эндпоинты с тяжёлой агрегацией. Их обычно надо не переписывать, а переносить в колоночную базу: в кейсе про LLM и аналитику рекламы агрегации, которые в приложении считались десятками секунд, ушли в готовые витрины ClickHouse и стали отвечать за миллисекунды. Смена языка тут не помогла бы вовсе.
Как ускорить Python backend без смены языка
Прежде чем заводить второй язык в стеке, стоит пройтись по короткому списку — он часто даёт кратный выигрыш за дни работы. Пример из практики: пересчёт цен со скидками по каталогу в 50 000 SKU на каждый запрос.
Плохо (чистые циклы Python, 2,4 секунды на запрос):
for sku in skus:
price = sku.base_price
for rule in discount_rules:
if rule.applies(sku):
price *= 1 - rule.percent / 100
sku.final_price = round(price, 2)
Хорошо (те же правила, но векторизованно — 310 мс):
prices = np.array([s.base_price for s in skus])
for rule in discount_rules:
mask = rule.mask(categories) # bool-массив на 50 000 позиций
prices[mask] *= 1 - rule.percent / 100
final = prices.round(2)
Восьмикратное ускорение без единой строчки на Go. Тот же принцип работает и на других слоях:
orjsonилиmsgspecвместо стандартногоjson— 3–8× на сериализации больших ответов, замена в одну строку.msgspecзаодно валидирует схему быстрее Pydantic.- Pydantic v2 вместо первой версии: ядро уже написано на Rust, валидация ускоряется в 5–20 раз. Вы получаете Rust, не написав ни строчки Rust.
- Polars вместо pandas на табличных расчётах — тоже Rust под капотом, обычно 5–15×.
- Кэш на уровне ответа (Redis) для эндпоинтов, где данные меняются реже, чем их запрашивают. Просто и очевидно, но по деньгам это самый выгодный шаг из всех.
uvloopи нормальный пул соединений к базе — единицы процентов, но бесплатно.
Если после этого прохода эндпоинт всё ещё упирается в CPU — тогда разговор про Go и Rust становится предметным.
Как обойти GIL в Python для highload
Отдельный вопрос, который стоит закрыть до миграции: GIL больше не приговор, но и не решённая проблема. С версии 3.14 (октябрь 2025) free-threaded-сборка CPython официально поддерживается — это уже не эксперимент, как в 3.13. Потоки в ней действительно исполняются параллельно на разных ядрах, и CPU-bound код в одном процессе масштабируется по ядрам без multiprocessing.
Три оговорки, из-за которых это пока не универсальный ответ для высоконагруженного бэкенда на Python:
- Free-threaded-сборка (
3.14t) не является сборкой по умолчанию — её нужно ставить осознанно, отдельным дистрибутивом или образом. - Однопоточный код в ней медленнее обычного на 5–10%: вы платите за параллелизм там, где он не нужен.
- Совместимость C-расширений всё ещё неполная. Ключевые пакеты колёса под
cp314tуже выпускают, но проверять придётся весь ваш список зависимостей, а не только топ-10.
Практический вывод: если узкое место — CPU-bound расчёт в веб-воркере, попробуйте 3.14t на стенде до того, как оценивать миграцию на Golang. Это дни работы против недель. А вот если проблема в памяти и в стоимости одного воркера, free-threading не поможет — снятие GIL не делает объекты Python компактнее.
Python vs Golang и Rust: где граница производительности
На I/O-эндпоинтах Go и Rust дают примерно 4–5-кратный запас по пропускной способности, на CPU-bound задачах — от 10 до 25 раз. Разрыв между самими Go и Rust при этом гораздо скромнее, чем принято думать.
Сценарий для замеров ниже — эндпоинт каталога интернет-магазина: выборка из Postgres, пересчёт цен и отдача 200 позиций в JSON.
FastAPI + asyncpg + orjson против Gin + pgx и Axum + SQLx. Postgres общий, время ответа базы вычтено. Порядок величин совпадает с публичными бенчмарками (3–9× между Python и Go); на вашей нагрузке цифры будут другими.
Здесь важна вторая цифра, а не первая: Rust быстрее Go на 23%, и в реальном сервисе эта разница почти целиком тонет в задержке базы и сети. А вот на чистом счёте картина меняется — тот самый пересчёт прайса на 50 000 SKU:
NumPy — тот же Python с векторизацией из примера выше. Он закрывает 87% разрыва до Go, не добавляя в стек второй язык.
Обратите внимание на вторую строку. Векторизованный Python отстаёт от Go всего в 1,6 раза — при том что не требует ни новой команды, ни второго CI-пайплайна, ни отдельного сервиса. Переписывание на Go имеет смысл, когда векторизовать нечего: сложная ветвящаяся логика, разбор нестандартных форматов, потоковая обработка.
Сводка по остальным характеристикам, которые влияют на эксплуатацию:
| Критерий | Python (FastAPI) | Go (Gin) | Rust (Axum) |
|---|---|---|---|
| RPS на ядро, I/O | 1× | 3,9× | 4,8× |
| CPU-bound расчёты | 1× | 12× | 25× |
| RAM на инстанс | 180–250 МБ | 30–60 МБ | 10–25 МБ |
| Холодный старт | 0,8–3 с | 20–50 мс | 5–20 мс |
| Предсказуемость p99 | GIL и всплески GC | паузы GC < 1 мс | без GC |
| Время сборки в CI | секунды | 20–60 с | 3–10 мин |
| Срок до первого прод-эндпоинта | 1× | 1,3–1,6× | 2,5–4× |
| Порог входа для команды | — | 2–4 недели | 2–4 месяца |
| Найм в РФ | легко | средне | сложно и дорого |
Строки про холодный старт и RAM важнее, чем кажется: если вы живёте в Kubernetes с автоскейлингом, Python-под, который поднимается три секунды и ест 200 МБ, диктует и запас по ресурсам, и скорость реакции на всплеск трафика.
Стек 2026 для высоконагруженного бэкенда на Go и Rust
Для коммерческой разработки в 2026 году набор устоявшийся — в обоих языках есть по варианту, вокруг которого больше всего документации, готовых интеграций и кандидатов на рынке.
| Слой | Python | Go | Rust |
|---|---|---|---|
| Веб-фреймворк | FastAPI | Gin (v1.x) | Axum 0.8 |
| ORM / доступ к БД | SQLAlchemy 2.0 | GORM v2 | SeaORM 2.0 |
| Сериализация | Pydantic v2 + orjson |
encoding/json v2 |
serde + serde_json |
| Для горячего пути | msgspec |
sqlc + pgx |
sqlx + simd-json |
Почему именно так:
- Gin — примерно у половины Go-разработчиков как основной фреймворк, работает поверх стандартного
net/http, поэтому совместим со всей экосистемой middleware. Fiber быстрее на синтетике, но живёт наfasthttp, не поддерживает HTTP/2 и не совместим сhttp.Handler— для долгоживущего сервиса это плохой размен. - GORM — самый популярный Go-ORM, привычный active record. Для эндпоинтов, ради которых вы всё и затеяли, лучше
sqlc: он генерирует типизированный Go-код из ваших SQL-запросов, без рефлексии в рантайме. encoding/jsonv2 в Go 1.26 стал стандартом и закрыл историческое отставание отsonicиgoccy/go-json. Отдельная библиотека для JSON в новом проекте больше не нужна.- Axum 0.8 — фреймворк от команды Tokio, де-факто стандарт в Rust: типобезопасные экстракторы, вся экосистема
towerдля middleware. Actix Web выдаёт на 10–15% больше RPS под предельной нагрузкой, но на реальном сервисе это незаметно. - SeaORM 2.0 (январь 2026) — async-ORM поверх SQLx с нормальными связями и eager loading. Если ORM не нужен, берите SQLx напрямую: он проверяет ваши SQL-запросы к реальной схеме на этапе компиляции.
- serde — безальтернативный стандарт сериализации в Rust, тот самый
#[derive(Serialize)].
Обработчик на Go получается почти таким же коротким, как на FastAPI — теги структуры описывают и таблицу, и JSON сразу:
type Item struct {
SKU string `json:"sku" gorm:"primaryKey"`
BasePrice float64 `json:"base_price"`
FinalPrice float64 `json:"final_price"`
}
func getPrices(c *gin.Context) {
var items []Item
db.Where("category_id = ?", c.Param("id")).Find(&items)
c.JSON(http.StatusOK, recalc(items))
}
На Rust кода примерно столько же, но компилятор требует явности во всём — включая типы ответа и состояние приложения:
#[derive(Serialize)]
struct Price { sku: String, final_price: f64 }
async fn get_prices(
Path(id): Path<i32>, State(db): State<DbConn>,
) -> Json<Vec<Price>> {
let items = Item::find()
.filter(item::Column::CategoryId.eq(id))
.all(&db).await.unwrap();
Json(recalc(items))
}
Разница в объёме кода небольшая — разница в сроках берётся не из синтаксиса, а из времени, которое команда тратит на borrow checker, async-траты и подбор крейтов на первых проектах.
Экономия на серверах: когда миграция с Python на Golang окупается
Считать надо две суммы: сколько вы экономите на железе в месяц и сколько разово стоит переписывание. Возьмём эндпоинт с постоянной нагрузкой 10 000 RPS и целевой утилизацией 50% (то есть закладываем двукратный запас).
Расчёт по ценам российских облаков 2026 года: ~1 100 ₽ за vCPU и ~300 ₽ за ГБ RAM в месяц. Python: 10,5 vCPU и 2,1 ГБ; Go: 2,7 vCPU и 0,4 ГБ; Rust: 2,2 vCPU и 0,15 ГБ. Без учёта резервирования по зонам.
Экономия на переходе с Python на Go — примерно 9 100 ₽ в месяц. Экономия на переходе с Go на Rust — 600 ₽ в месяц, то есть меньше часа работы разработчика. Это одна из главных причин, почему для большинства бизнес-задач правильный выбор — Go, а не Rust.
Теперь разовые затраты. Перенос одного эндпоинта — это не только код: нужны тесты, сравнение ответов со старой версией, деплой, метрики и дашборды, обучение команды.
Go — 6 человеко-дней, Rust — 14, по ставке senior-разработчика 25 000 ₽ в день (подряд или аутстафф, 2026). В оценку входят тесты, параллельный запуск со сверкой ответов, деплой и мониторинг. Для команды, у которой это первый сервис на данном языке, срок стоит умножить на 1,5–2.
Сводим в срок окупаемости и получаем вывод:
| Экономия, ₽/мес | Стоимость переноса | Окупаемость на железе | |
|---|---|---|---|
| Python → Go | 9 100 | 150 000 ₽ | ~16 месяцев |
| Python → Rust | 9 700 | 350 000 ₽ | ~36 месяцев |
| Go → Rust | 600 | 350 000 ₽ | никогда |
На экономии железа переписывание почти никогда не окупается — на масштабе среднего бизнеса это игра вдолгую с горизонтом в полтора года, за которые API успеют переписать пару раз по продуктовым причинам. Считать нужно другое:
- Выручку. Если сокращение p95 с 800 до 120 мс поднимает конверсию оформления заказа хотя бы на 0,5%, при обороте 50 млн ₽ в месяц это 250 000 ₽ — окупаемость меняется с месяцев на недели.
- Штрафы по SLA. Если в договоре зафиксирован потолок в 200 мс на p99, вопрос «окупится ли» вообще не стоит.
- Потолок масштабирования. Когда счёт за бэкенд перевалил за 500 000 ₽ в месяц, те же 4× по железу превращаются в 4,5 млн ₽ в год — и арифметика становится совсем другой.
Грубый ориентир: пока расходы на бэкенд-инфраструктуру меньше 50 000 ₽ в месяц, переписывать ради экономии не нужно вообще никогда — почините базу и кэш. Сокращение облачных расходов на этом масштабе дешевле достигается правильным типом инстансов и вынесенным кэшем, чем вторым языком в стеке.
Есть и обратная статья расходов, про которую забывают: второй язык стоит денег постоянно. Сборка и образы в CI, отдельные дашборды и алерты, участие людей, которые умеют читать новый стек, найм под него. По нашему опыту, стоимость поддержки Python против Go по эксплуатации сопоставима, а вот кадровая часть — нет: закрыть вакансию Go-разработчика в РФ дольше и дороже, чем Python, а Rust — дороже в разы. Закладывайте это в расчёт вместе с ценой vCPU.
Когда нужно переходить с Python на другой язык, а когда нет
Решение принимается не по языку целиком, а по каждому узлу отдельно: перенос микросервиса с Python на Go оправдан ровно там, где для него есть измеренная причина. Короткая таблица «симптом → действие», по которой удобно проверять конкретный эндпоинт:
| Что вы видите | Что делать |
|---|---|
| 90% времени ответа — ожидание базы | Индексы, устранение N+1, кэш. Язык ни при чём |
| 2% трафика дают 70% CPU кластера | Кандидат №1 на перенос: одна точка, измеримый эффект |
| Тяжёлый расчёт в цикле на каждый запрос | Сначала векторизация, потом Go |
| Разбор больших XML/CSV, сжатие, изображения, крипта | Rust через PyO3 — здесь его отрыв реален |
| Десятки тысяч WebSocket-соединений | Go: горутины дешевле, чем воркеры Python |
| Жёсткий SLA по p99 (платежи, антифрод, торги) | Go или Rust, вопрос окупаемости не стоит |
| Агент или сайдкар на каждый хост | Go или Rust: один бинарник, десятки МБ RAM |
| Логика меняется каждый спринт | Оставить на Python: скорость изменений дороже |
| Эндпоинт вызывают 50 раз в сутки | Не трогать, даже если он «неоптимальный» |
| Всё завязано на pandas, PyTorch, scikit-learn | Не трогать: под капотом уже C и CUDA |
| В команде нет ни одного Go-разработчика | Сначала решить кадровый вопрос, потом технический |
| Профиля нагрузки нет, есть ощущение «тормозит» | Профилировать. Всё остальное — гадание |
Последняя строка встречается чаще всех остальных вместе взятых.
Гибридный подход: интеграция Python и Rust через PyO3 вместо переписывания сервиса
Оптимальная стратегия почти всегда одна и та же — оставить Python как основной язык продуктовой разработки и вынести на Go или Rust только измеренные горячие точки. Тремя ступенями, по возрастанию цены.
Ступень 1 — нативное расширение Python на Rust (PyO3). Самый дешёвый вариант: переписывается одна функция, а не сервис. Ни нового деплоя, ни сетевого вызова, ни отдельного репозитория.
#[pyfunction]
fn recalc_prices(prices: Vec<f64>, percent: f64) -> Vec<f64> {
prices.iter().map(|p| p * (1.0 - percent / 100.0)).collect()
}
Со стороны Python это обычный импорт — вызывающий код не меняется вообще:
from pricing_core import recalc_prices # скомпилированный Rust-модуль
final = recalc_prices(base_prices, discount_percent)
Ровно так устроены Pydantic v2, Polars и orjson — вы, скорее всего, уже пользуетесь этим подходом, просто чужими руками. Сборка через maturin, колёса собираются в CI, разработчикам продукта знать про Rust не нужно.
Ступень 2 — отдельный сервис за тем же гейтвеем. Когда горячая точка — целый эндпоинт с собственными запросами к базе. Гейтвей (nginx, Traefik, API Gateway) маршрутизирует по путям, клиенты ничего не замечают:
GET /api/v1/catalog/search → Go (Gin) — 4 200 rps, 71% CPU кластера
GET /api/v1/prices/bulk → Go (Gin) — CPU-bound пересчёт
POST /api/v1/orders → Python (FastAPI) — 12 rps, логика меняется еженедельно
* → Python (FastAPI)
Это классический strangler fig: новый сервис откусывает по одному маршруту, старый продолжает работать. Обязательное условие — период параллельной работы, когда оба обработчика получают один и тот же трафик, а их ответы сравниваются автоматически. Расхождения находятся именно там, и почти всегда в округлении денег и часовых поясах.
Когда Go-сервис вызывается не снаружи, а из самого Python-монолита, связка Python и Go делается через gRPC: один .proto порождает и Go-сервер, и Python-клиент, а бинарный protobuf экономит те самые миллисекунды, ради которых всё затевалось.
service Pricing {
rpc RecalcBulk (RecalcRequest) returns (RecalcResponse);
}
Важная оговорка про то, что часто ищут отдельно: горутину из Python «вызвать» нельзя — это конструкция рантайма Go, а не функция. Общаться с Go-кодом можно либо по сети (gRPC, HTTP), либо через cgo-обёртку над скомпилированной библиотекой. Первый способ проще и переживает деплои; второй тащит в Python-процесс чужой рантайм с собственным сборщиком мусора — для нативных расширений эту роль лучше отдаёт Rust.
Ступень 3 — полный переезд. Оправдан редко: когда на Python остались только тонкие обёртки, а команда уже год как пишет на Go. Планировать это заранее не нужно — если первые две ступени сделаны правильно, третья либо случится сама, либо окажется ненужной.
Практическое правило по границе: выносите наружу то, что можно описать чистым контрактом «вход → выход» и что не тянет за собой половину доменной логики. Пересчёт цен, поиск по каталогу, генерация выгрузок, обработка вебхуков — хорошие кандидаты. Оформление заказа с десятком интеграций и правил — плохой.
Что дальше
- Если тормозит поиск или выдача каталога, начните не с языка, а с архитектуры поиска — разбор в статье «RAG для каталога товаров».
- Если узкое место — персонализация и ранжирование, полезно сначала проверить, хватает ли вам вообще данных для эффекта: «Почему персональные рекомендации не работают».
- Если тяжёлые эндпоинты аналитические, посмотрите, как выглядит вынос агрегаций в ClickHouse в кейсе про LLM и аналитику рекламы.
Коротко
Стоит ли переписывать бэкенд с Python на Go или Rust?
Целиком — почти никогда, а вот отдельные узлы бывает нужно: эндпоинты, съедающие большую часть CPU, CPU-bound расчёты и сервисы с жёстким SLA. В остальных случаях выгоднее починить базу, кэш и алгоритмы.
Что быстрее для бэкенда — Go или Rust?
На I/O-эндпоинте разница между ними 20–25%, и её съедает база с сетью. Rust заметно отрывается только на чистом CPU, при этом стоит в разработке примерно вдвое дороже Go.
Сколько стоит перенести один критичный эндпоинт с Python на Go?
5–8 человеко-дней с тестами, деплоем и мониторингом — порядка 150 000 ₽ по ставкам 2026 года. На Rust — 12–16 дней. Плюс постоянные расходы на второй язык в CI, дежурствах и найме.
Как понять, какие участки API нужно переписывать?
По профилю нагрузки: APM с разбивкой по эндпоинтам, флеймграф под нагрузкой и деление времени ответа на «ждём базу» и «считаем сами». Кандидат — эндпоинт с единицами процентов трафика и десятками процентов CPU.
Как ускорить Python backend без смены языка?
Индексы и устранение N+1, кэш, orjson или msgspec, векторизация через NumPy или Polars, Pydantic v2. Суммарно это даёт 3–10× на горячем коде за дни работы, а для CPU-bound кода стоит проверить ещё и free-threaded-сборку CPython.
Как обойти GIL в Python для highload?
С версии 3.14 free-threaded-сборка официально поддерживается, и потоки в ней работают параллельно на разных ядрах. Но это не сборка по умолчанию, однопоточный код медленнее на 5–10%, а совместимость C-расширений пока неполная.
Если хочется понять, где именно в вашем API теряются миллисекунды и что из этого имеет смысл переносить, — пишите нам через форму на сайте: проведём аудит узких мест, покажем разбивку времени ответа по эндпоинтам и посчитаем окупаемость до того, как что-то переписывать.