Транскрибация

Rust + Axum для SaaS: lessons learned из production API

Почему мы выбрали Rust и Axum для бэкенда TEOM. Реальный опыт: от проектирования до production, что сработало, а что оказалось сложнее, чем казалось.

14 июля 2026 г.·15 мин чтения
← Все статьи

Введение: почему именно Rust для SaaS

Каждый стартап рано или поздно сталкивается с выбором стека для бэкенда. JavaScript? Python? Go? Rust? На первый взгляд, Rust — странное решение для SaaS: сложный синтаксис, крутая кривая обучения, маленькое сообщество по сравнению с тем же Python. Но за этой сложностью стоит конкретная выгода: скорость, типобезопасность и предсказуемость поведения в production. В TEOM мы обрабатываем аудио и видео для транскрибации — задачу, где каждая миллисекунда на счету. 80-минутная запись психотерапевтической сессии обрабатывается за 193 секунды — это в 25 раз быстрее реального времени. За этим стоит не только оптимизация моделей (GigaAM с WER 23.9%), но и правильный выбор инфраструктуры. В этой статье расскажу наш путь от выбора стека до работы в production: что получилось, что оказалось сложнее, и почему Axum стал нашим основным фреймворком.

Определения: термины, которые понадобятся

Прежде чем начать, определю несколько терминов, которые будут встречаться в статье:
  • Axum — веб-фреймворк для Rust от команды Tokio. Строится поверх hyper (низкоуровневая HTTP-библиотека) и tower (абстракция для middleware). Сочетает производительность с удобным API для маршрутизации и обработки запросов.
  • SQLx — асинхронная библиотека для работы с PostgreSQL из Rust. В отличие от других драйверов, проверяет SQL-запросы на этапе компиляции — ошибки в базе данных ловятся не в runtime, а при сборке.
  • Tokio — асинхронный runtime для Rust. Предоставляет event loop, таймеры, каналы связи и таски (лёгкие потоки выполнения). Всё асинхронное в Rust работает поверх Tokio.
  • SERDE — фреймворк для сериализации и десериализации данных. Превращает Rust-структуры в JSON, YAML или другие форматы и обратно. Используется повсеместно.
  • Tower — абстракция для построения сетевых сервисов. Предоставляет слои (layers), которые оборачивают обработчики: логирование, аутентификация, rate limiting и другие middleware.
  • WER (Word Error Rate) — метрика точности распознавания речи. Показывает процент слов, которые модель распознала неправильно. Чем ниже, тем лучше.
  • Middleware — промежуточное ПО, которое обрабатывает запрос до или после основного обработчика. Используется для логирования, аутентификации, кэширования и других поперечных задач.
  • Сериализация — процесс превращения данных из внутреннего представления программы в формат для передачи (например, JSON).
  • Таск — лёгкая единица выполнения в асинхронном runtime. В отличие от потока, таск не привязан к ядру процессора и переключается операционной системой при необходимости.
  • Connection pooling — техника переиспользования соединений с базой данных вместо создания нового для каждого запроса. Снижает накладные расходы на установку соединения.

Акт I: Зов приключения — зачем менять Python на Rust

TEOM начался с Python-бэкенда. Flask, SQLAlchemy, простая архитектура. И пока мы обрабатывали 10-минутные записи — всё было нормально. Скорость обработки — 15-20 минут на запись, несколько параллельных задач, пользователи ждут. Но когда мы перешли на 60-80-минутные записи, Python начал задыхаться. Вот конкретные проблемы:

Проблема 1: блокирующий ввод-вывод

Python-сервер блокируется при ожидании ответа от ML-модели. Каждый запрос на транскрибацию занимал рабочий поток на 5-15 минут. Пул потоков исчерпывался, новые запросы стояли в очереди. ``python

Проблемный код на Python

@app.route('/transcribe', methods=['POST']) def transcribe(): audio = request.files['audio'] # Блокирующий вызов — поток простаивает result = gigaam.transcribe(audio) # 5-15 минут! return jsonify(result)
`

Проблема 2: потребление памяти

Python-процесс с загруженной моделью GigaAM потреблял 4-6 ГБ RAM. При масштабировании на несколько экземпляров — сервер просто не влезал в память.

Проблема 3: ошибки в runtime

Python не ловит ошибки типов на этапе разработки. Опечатка в имени поля структуры — это runtime-ошибка, которая проявляется только при обработке конкретного запроса. В production, когда пользователи ждут результат, это критично. Мы попробовали Go — получилось быстрее, но не настолько, как хотелось бы. А когда столкнулись с проблемами параллелизма в ML-пайплайне, поняли: нужен язык с настоящими асинхронными примитивами и нулевой стоимостью абстракций.

Акт II: Встреча с наставником — выбор Axum

Rust не был очевидным выбором. Мы рассматривали Actix Web, Rocket и Tide. Но Axum выделялся нескольким вещами:

Поддержка сообщества Tokio

Axum создан и поддерживается командой Tokio — того самого runtime, который используется в большинстве асинхронных Rust-проектов. Это значит, что документация, примеры и решения проблем всегда актуальны. Если вы гуглите «как сделать X в Axum», ответ почти наверняка есть.

Отсутствие макросов-волшебства

В отличие от Rocket, который использует attribute-макросы для маршрутизации ( #[get("/")] ), Axum строится на обычных функциях и замыканиях. Это упрощает отладку: вы видите точный стек вызовов, без «волшебных» преобразований, которые сложно отслеживать.

Экстракторы вместо валидации

Axum использует систему экстракторов: вы описываете, какие данные ожидает обработчик, и фреймворк сам парсит и валидирует их. Ошибки — это не исключения, а типизированные значения.
`rust use axum::{extract::Json, http::StatusCode, response::IntoResponse}; use serde::{Deserialize, Serialize}; #[derive(Deserialize)] struct TranscribeRequest { audio_url: String, language: Option, // "ru" по умолчанию } #[derive(Serialize)] struct TranscribeResponse { text: String, duration_seconds: f64, word_count: usize, } async fn transcribe( Json(request): Json, ) -> Result, StatusCode> { // request уже распарсен и валидирован // Ошибка типов невозможна — компилятор проверил let result = ml_service.transcribe(&request.audio_url).await; match result { Ok(text) => Ok(Json(TranscribeResponse { text, duration_seconds: 0.0, word_count: 0, })), Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR), } } `

Tower: middleware как слои

Tower предоставляет простой способ добавлять middleware. Логирование, аутентификация, rate limiting — всё строится как слои, которые оборачивают обработчик.
`rust use tower_http::trace::TraceLayer; use tower_http::cors::CorsLayer; let app = Router::new() .route("/v1/transcribe", post(transcribe_handler)) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()); `

Конвейерная композиция

Axum позволяет компоновать маршруты и слои как конструктор. Каждый роут — это функция, которая принимает состояние и возвращает роутер. Это делает архитектуру модульной и тестируемой.

Акт III: Испытания и порог — production-проблемы

Испытание 0: обработка ошибок

В Rust нет исключений (exceptions). Вместо этого используется тип Result — либо значение, либо ошибка. Это заставляет явно обрабатывать каждый возможный сбой. В Axum это выглядит так: обработчик возвращает Result, AppError>, где AppError — наш собственный тип ошибки. Мы реализовали для него IntoResponse, чтобы ошибки автоматически превращались в HTTP-ответы с правильными кодами и сообщениями.
`rust #[derive(Debug)] enum AppError { NotFound(String), ValidationError(String), InternalError(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg), }; let body = Json(serde_json::json!({ "error": message })); (status, body).into_response() } } ` Такой подход гарантирует, что мы не забыли обработать ни одной ошибки. Если функция может упасть — компилятор заставит вас это учесть.

Испытание 1: долгие ML-запросы

Первая серьёзная проблема: ML-модели (GigaAM, PaddleOCR) выполняются долго — от 2 до 15 минут. Axum по умолчанию обрабатывает запросы в тасках Tokio. Но если ML-операция блокирует таск, весь runtime замедляется. Решение: выносим ML-операции в отдельные таски с ограничением параллелизма.
`rust use tokio::sync::Semaphore; use std::sync::Arc; // Ограничиваем количество параллельных ML-операций let semaphore = Arc::new(Semaphore::new(4)); // максимум 4 одновременно async fn transcribe_with_limit( request: TranscribeRequest, semaphore: Arc, ) -> Result { let _permit = semaphore.acquire().await .map_err(|_| "Сервис перегружен".to_string())?; // ML-операция выполняется в отдельном таске let result = tokio::task::spawn_blocking(move || { gigaam.transcribe(&request.audio_url) }).await.map_err(|e| e.to_string())?; Ok(result) } `

Испытание 2: работа с PostgreSQL

SQLx проверяет SQL-запросы на этапе компиляции. Это огромное преимущество: ошибки в запросах ловятся не в production, а при
cargo build. Но есть нюанс: запросы должны быть известны на этапе компиляции. Для динамических запросов (фильтрация, сортировка, пагинация) мы используем sqlx::query_as с ручной сборкой запроса: `rust use sqlx::PgPool; async fn get_user_transcriptions( pool: &PgPool, user_id: i64, limit: i64, offset: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( Transcription, r#" SELECT id, user_id, audio_url, text, created_at FROM transcriptions WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3 "#, user_id, limit, offset ) .fetch_all(pool) .await } `

Испытание 3: стриминг ответов

При транскрибации длинных записей пользователь хочет видеть прогресс. Мы реализовали стриминг через Server-Sent Events (SSE): каждые несколько секунд клиент получает обновление о текущем статусе.
`rust use axum::response::sse::{Event, Sse}; use futures::stream::Stream; use std::convert::Infallible; async fn transcribe_stream( request: Json, ) -> Sse>> { let stream = async_stream::stream! { yield Ok(Event::default().data("Начинаю обработку...")); // Промежуточные обновления for progress in 0..100 { tokio::time::sleep(Duration::from_secs(5)).await; yield Ok(Event::default() .data(format!("Прогресс: {}%", progress))); } yield Ok(Event::default().data("Готово!")); }; Sse::new(stream) } `

Акт IV: Награда — результаты в production

После шести месяцев разработки и двух месяцев в production, вот что мы имеем:

Тестирование

Rust-экосистема предоставляет мощные инструменты для тестирования. Мы написали интеграционные тесты для каждого API-эндпоинта, которые проверяют не только happy path, но и граничные случаи: пустые запросы, невалидные данные, превышение лимитов. Команда cargo test запускает все тесты за несколько секунд. В сравнении с Python, где pytest может работать минуты — это огромная разница. Более того,Потому что мы используем sqlx::query_as, изменения схемы базы данных отлавливаются на этапе компиляции, а не во время запуска тестов.
`rust #[tokio::test] async fn test_transcribe_endpoint() { let app = create_test_app().await; let response = app .oneshot( Request::builder() .uri("/v1/transcribe") .method("POST") .header("content-type", "application/json") .body(Body::from(serde_json::json!({ "audio_url": "https://example.com/audio.wav" }).to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } `

Деплой

Мы развернули TEOM на Docker Compose с следующими сервисами (подробнее о выборе между ):
  • PostgreSQL (pgvector:pg16) — основная база данных, порт 5432
  • Redis — кэш и очередь задач
  • MinIO — объектное хранилище для аудио- и видеофайлов
  • Axum API — наш основной бэкенд
  • ML-воркер — отдельный процесс для инференса моделей
Docker-образ нашего API весит всего 45 МБ (против 800 МБ для Python-версии). Это значит, что деплой занимает секунды, а не минуты. Автоматическое масштабирование работает проще: контейнер запускается быстро, потребляет мало ресурсов.

Производительность

  • Обработка 80-минутной записи: 193 секунды (было 15-20 минут на Python)
  • Задержка ответа (p95): 120 мс (было 800 мс на Python)
  • Потребление памяти: 1.2 ГБ на экземпляр (было 4-6 ГБ)
  • Максимальная нагрузка: 50 параллельных транскрибаций на одном сервере

Надёжность

  • Точность транскрибации: GigaAM показал WER 23.9% на нашем бенчмарке — значительно лучше Whisper (35.7%)
  • Отказоустойчивость: типовая система Rust ловит ошибки на этапе компиляции, которые в Python привели бы к крашу в production
  • Логирование: полная трассировка каждого запроса через Tower middleware

Скорость разработки

Парадоксально, но после изучения Rust мы стали разрабатывать быстрее. Компилятор ловит ошибки до запуска, а типизированный код самодокументируется. PR-ревью занимают меньше времени, потому что компилятор уже проверил большую часть логики.

Акт V: Возвращение — что бы мы сделали иначе

Не начинать с монолита

В первые месяцы мы пытались уместить всё в один бинарник: API, ML-инференс, фоновые задачи, . Это было ошибкой. В итоге разделили на микросервисы: API-сервис (Axum), ML-воркер (отдельный процесс), фоновые задачи (Redis Queue).

Раньше использовать Connection Pooling

SQLx предоставляет пул соединений к PostgreSQL, но мы подключили его только на третий месяц разработки. Ранее каждый запрос создавал новое соединение — это добавляло 50-100 мс к каждому запросу. Для API с десятками запросов в секунду это было критично.

Не бояться unsafe

Rust запрещает небезопасный код по умолчанию. Но для интеграции с C-библиотеками (ffmpeg, модели) иногда необходим
unsafe`. Мы слишком долго пытались обойтись без него, в итоге написали лишние тысячи строк кода.

Инвестировать в документацию раньше

Rust-код самодокументируется лучше, чем Python или JavaScript, но это не значит, что документация не нужна. Мы слишком долго откладывали написаниеREADME, что потом затрудняло онboarding новых разработчиков.

Заключение: Rust как инвестиция

Rust + Axum — это инвестиция в будущее. Кривая обучения крутая: на освоение уходит 2-3 месяца. Но потом вы получаете язык, который:
  • Не даёт писать небезопасный код (без явного разрешения)
  • Ловит ошибки на этапе компиляции
  • Обеспечивает производительность на уровне C/C++
  • Имеет отличную экосистему для асинхронного программирования
Для SaaS, где важны скорость, надёжность и масштабируемость — это правильный выбор. Мы в TEOM обрабатываем миллионы минут аудио, и каждый раз удивляемся, насколько быстро и стабильно работает наш стек.

Попробуйте TEOM

Хотите увидеть, как Rust + Axum работают в деле? TEOM — это платформа для транскрибации аудио и видео с использованием модели GigaAM. Обрабатывайте записи любой длины, получайте точные расшифровки с метками спикеров и анализируйте содержание. 80 минут аудио — 193 секунды обработки. Без компромиссов в качестве. Попробуйте TEOM бесплатно на teom.io — начните с одной записи и убедитесь сами.
rustaxumsaasbackendтранскрибацияproduction

Попробуйте TEOM бесплатно

Загрузите файл и получите результат за секунды.