Rust para backend: axum y tokio en servicios reales
Índice de contenidos
- Puntos clave
- El stack actual
- Un endpoint mínimo en axum
- Por qué la curva de aprendizaje pesa
- Comparativa pragmática
- Casos donde compensa
- Patrones que ahorran sufrimiento
- Conclusión
- Preguntas frecuentes
- ¿Merece la pena Rust para un servicio CRUD típico sobre Postgres?
- ¿Cuánto tarda en compilar un backend en Rust y cómo puedo acelerarlo?
- ¿Qué crates uso para errores y logs en un servicio axum?
- Fuentes
Rust ya no es solo un lenguaje de sistemas: con tokio como runtime asíncrono y axum como framework HTTP se construyen servicios backend de alto rendimiento con verificación de tipos en compilación. Compensa en gateways, proxies y procesadores de eventos; para CRUD típico sobre Postgres, Go o Node.js siguen siendo más productivos.
Durante años Rust se posicionó como lenguaje para sistemas: sistemas operativos, drivers, motores de base de datos. Esa percepción ya está desfasada. Con tokio[1] como runtime asíncrono y axum[2] como framework HTTP, escribir servicios web en Rust es razonablemente cómodo, y la diferencia de rendimiento frente a Go o Node.js es real.
Puntos clave
-
El stack tokio + axum + serde + sqlx es la combinación dominante para backend en Rust: rendimiento, seguridad de tipos y ergonomía razonables una vez superada la curva inicial.
-
La curva de aprendizaje es real: ownership, lifetimes y async en Rust desconciertan durante los primeros meses.
-
Rust gana claramente en gateways, proxies y procesadores de eventos; para CRUD típico, Go o Node.js son más productivos.
-
Herramientas como
anyhow,tracing,moldycargo nextestreducen significativamente la fricción de desarrollo. -
La compilación lenta es el mayor inconveniente operativo: un proyecto medio compila en 30-60s en debug.
El stack actual
Cuando se habla de "backend en Rust", casi siempre se refiere a esta combinación:
-
tokio: el runtime async dominante. Maneja un número alto de tareas concurrentes con un número pequeño de hilos del SO usando un scheduler work-stealing.
-
axum: framework HTTP construido sobre tower[3] y hyper[4]. Mantenido por el equipo de tokio.
-
serde: el estándar de facto para serializar y deserializar JSON, YAML, TOML y otros formatos.
-
sqlx o diesel: para acceso a base de datos. sqlx, con verificación en tiempo de compilación de queries SQL, es la opción que más crece.
Combinado, este stack permite construir un microservicio JSON sobre Postgres en cantidad similar de código a un equivalente Go, con mejor seguridad de tipos y errores más informativos en compilación.
Un endpoint mínimo en axum
use axum::{routing::get, Router, Json};
use serde::Serialize;
#[derive(Serialize)]
struct Health { status: &'static str }
async fn health() -> Json<Health> {
Json(Health { status: "ok" })
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/health", get(health));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Tres conceptos clave en este ejemplo:
-
#[tokio::main]arranca el runtime async para que elmainsea una funciónasync. -
Los handlers son funciones
asyncnormales: axum se encarga del binding entre tipos y respuestas HTTP. -
Json<T>es un extractor: como tipo de retorno, axum serializa automáticamente a JSON con el header correcto.
Por qué la curva de aprendizaje pesa
No conviene minimizarlo: Rust tiene una curva de aprendizaje real. Para alguien que viene de Go o TypeScript, los primeros meses son difíciles. Los puntos de fricción principales:
-
Ownership y lifetimes. Rust no recolecta basura: el compilador valida que cada referencia sea válida. Son conceptos nuevos para la mayoría de desarrolladores.
-
Async en Rust. Los
Futureson lazy y dependen del runtime. Errores de compilación con tiposPin<Box<dyn Future...>>desconciertan al principio. -
Errores explícitos. Sin excepciones: cada función fallible devuelve
Result<T, E>y se propaga con?. Requiere cambio de mentalidad. -
Compilación lenta. Un proyecto medio compila en 30-60 segundos en debug y varios minutos en release. Ralentiza el ciclo iterativo.
Para superar esto conviene aceptar 2-3 meses de productividad reducida. Equipos sin esa paciencia suelen abandonar antes de ver los beneficios.
Logotipo de Rust, el lenguaje cuya arquitectura de ownership y async runtime (tokio) permite construir servicios backend de alto rendimiento sin GC (Imagen: ™/®Rust Foundation, CC BY 4.0, vía Wikimedia Commons)
Comparativa pragmática
| Aspecto | Rust + axum | Go + net/http | Node + Express |
|---|---|---|---|
| Latencia p99 baja | Excelente | Muy buena | Aceptable |
| Uso de memoria | Muy bajo | Bajo | Medio-alto |
| Curva de aprendizaje | Empinada | Suave | Fácil |
| Velocidad de compilación | Lenta | Rápida | N/A (interpretado) |
| Ecosistema HTTP | Maduro y creciendo | Muy maduro | Amplísimo |
| Productividad para CRUD | Media | Alta | Muy alta |
Rust gana donde el rendimiento por nodo importa de verdad: servicios bajo carga sostenida, consumo de mensajes de Kafka a tasas altas, gateways que multiplexan miles de conexiones. En servicios CRUD típicos donde el cuello de botella es la base de datos, la ganancia es marginal y el coste de aprendizaje no compensa.
Casos donde compensa
Cuatro tipos de servicios donde Rust se adopta con éxito en producción:
-
Gateways y proxies. Manejar 50.000 conexiones simultáneas con uso de memoria predecible.
-
Procesadores de eventos. Kafka consumers con throughput alto donde Go ya empezaba a notar la presión del GC.
-
Servicios CPU-bound. Procesamiento de imágenes, parsing de logs voluminosos, transformaciones complejas.
-
Componentes embebidos en otros lenguajes. Compilar a librería estática consumida desde Python o Node.js vía FFI.
Para el típico "validar JSON, hacer 3 queries a Postgres, devolver respuesta", Go o Node.js siguen siendo opciones más productivas.
Patrones que ahorran sufrimiento
Cinco consejos para equipos que adoptan Rust en backend:
-
anyhowpara errores en aplicación,thiserrorpara errores de librería. Hace la propagación de errores razonable sin boilerplate. -
tracingpara logs estructurados. Funciona bien con OpenTelemetry y se integra con axum via middleware. -
towermiddleware. Composición uniforme para logging, autenticación y rate limiting reutilizables. -
Build incremental con
moldcomo linker. Reduce tiempos de iteración significativamente en Linux. -
cargo nextest. Un test runner mucho más rápido quecargo testpor defecto.
Para la capa de caché sobre Postgres en servicios axum, Redis con estrategias cache-aside es la combinación más habitual. El stack de observabilidad con Grafana integra bien con tracing vía OTLP. Los contenedores de servicios Rust son más fáciles de operar con Podman en entornos rootless.
Conclusión
Rust en backend es una opción seria, no una promesa futura. La combinación tokio + axum es estable, performante y razonablemente productiva una vez superada la curva inicial. Pero no es la respuesta correcta para cualquier servicio: es una elección con trade-offs claros que conviene hacer conscientemente, basándose en cargas de trabajo reales y no en hype.
Esta guía también está disponible en inglés: Rust for Backend: axum and tokio in Real Services.
Fuentes:
- tokio: documentación oficial del runtime asíncrono[1]
- axum en GitHub: repositorio oficial del framework HTTP[2]
- tower en GitHub: la biblioteca de middleware sobre la que se construye axum[3]
- hyper: biblioteca HTTP de bajo nivel para Rust[4]
Preguntas frecuentes
¿Merece la pena Rust para un servicio CRUD típico sobre Postgres?
Normalmente no. En el servicio de validar JSON, hacer tres queries a Postgres y devolver respuesta, el cuello de botella es la base de datos, la ganancia de Rust es marginal y el coste de aprendizaje no compensa; Go o Node.js siguen siendo más productivos. Rust gana donde importa el rendimiento por nodo: gateways y proxies con 50.000 conexiones simultáneas, consumers de Kafka con throughput alto donde Go ya notaba la presión del GC, servicios CPU-bound y componentes embebidos en Python o Node.js vía FFI.
¿Cuánto tarda en compilar un backend en Rust y cómo puedo acelerarlo?
Un proyecto medio compila en 30-60 segundos en debug y varios minutos en release, y es el mayor inconveniente operativo porque ralentiza el ciclo iterativo. Dos herramientas reducen la fricción: mold como linker para builds incrementales, que recorta los tiempos de iteración de forma significativa en Linux, y cargo nextest, un test runner mucho más rápido que el cargo test por defecto. Aun así, conviene asumir 2-3 meses de productividad reducida mientras el equipo interioriza ownership, lifetimes y async.
¿Qué crates uso para errores y logs en un servicio axum?
Para errores, anyhow en código de aplicación y thiserror en librerías: hacen razonable la propagación con ? sin boilerplate. Para logs estructurados, tracing, que funciona bien con OpenTelemetry, se integra con axum vía middleware y encaja con el stack de Grafana a través de OTLP. El middleware de tower da una composición uniforme para logging, autenticación y rate limiting reutilizables. Para la caché sobre Postgres, la combinación más habitual es Redis con estrategias cache-aside.