Rust for Backend: axum and tokio in Real Services
Table of contents
- Key takeaways
- The current stack
- A minimal axum endpoint
- Why the learning curve matters
- Pragmatic comparison
- Cases where it pays off
- Patterns that save suffering
- Conclusion
- Frequently asked questions
- Is Rust worth it for a typical CRUD service over Postgres?
- How long does a Rust backend take to compile, and how can I speed it up?
- Which crates should I use for errors and logging in an axum service?
- Sources
Rust is no longer just a systems language: with tokio as the async runtime and axum as the HTTP framework, teams build high-performance backend services with compile-time type checking. It pays off in gateways, proxies and event processors; for typical CRUD over Postgres, Go or Node.js remain more productive.
For years Rust was positioned as a language for systems programming: operating systems, drivers, database engines. That perception is now outdated. With tokio[1] as the async runtime and axum[2] as the HTTP framework, writing web services in Rust is reasonably comfortable, and the performance difference vs Go or Node.js is real.
Key takeaways
-
The tokio + axum + serde + sqlx stack is the dominant combination for backend in Rust: performance, type safety, and reasonable ergonomics once past the initial curve.
-
The learning curve is real: ownership, lifetimes, and async in Rust take months to internalise.
-
Rust wins clearly for gateways, proxies, and event processors; for typical CRUD, Go or Node.js are more productive.
-
Tools like
anyhow,tracing,mold, andcargo nextestsignificantly reduce development friction. -
Slow compilation is the biggest operational drawback: a medium project compiles in 30-60s in debug mode.
The current stack
When people talk about "backend in Rust", they almost always mean this combination:
-
tokio: the dominant async runtime. Handles a high number of concurrent tasks with a small OS thread pool using a work-stealing scheduler.
-
axum: HTTP framework built on tower[3] and hyper[4]. Maintained by the tokio team.
-
serde: the de facto standard for serialising and deserialising JSON, YAML, TOML, and other formats.
-
sqlx or diesel: for database access. sqlx, with compile-time SQL query verification, is the option growing fastest.
Combined, this stack lets you build a JSON microservice over Postgres in similar code volume to a Go equivalent, with better type safety and more informative compile errors.
A minimal axum endpoint
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();
}
Three key concepts in this example:
-
#[tokio::main]boots the async runtime somaincan beasync. -
Handlers are normal
asyncfunctions: axum handles the binding between types and HTTP responses. -
Json<T>is an extractor: as a return type, axum automatically serialises to JSON with the correct header.
Why the learning curve matters
Don’t downplay it: Rust has a real learning curve. For someone coming from Go or TypeScript, the first months are hard. Main friction points:
-
Ownership and lifetimes. Rust doesn’t garbage-collect: the compiler validates each reference is valid. These are new concepts for most developers.
-
Async in Rust.
Futures are lazy and depend on the runtime. Compile errors withPin<Box<dyn Future...>>confuse at first. -
Explicit errors. No exceptions: every fallible function returns
Result<T, E>and propagates with?. Requires a mindset shift. -
Slow compilation. A medium project compiles in 30-60 seconds in debug and several minutes in release. Slows the iteration cycle.
Accept 2-3 months of reduced productivity to get through this. Teams without that patience usually drop out before seeing the benefits.
Rust logo, the language whose ownership architecture and async runtime (tokio) enable building high-performance backend services without GC (Image: ™/®Rust Foundation, CC BY 4.0, via Wikimedia Commons)
Pragmatic comparison
| Aspect | Rust + axum | Go + net/http | Node + Express |
|---|---|---|---|
| Low p99 latency | Excellent | Very good | Acceptable |
| Memory usage | Very low | Low | Medium-high |
| Learning curve | Steep | Smooth | Easy |
| Compile speed | Slow | Fast | N/A (interpreted) |
| HTTP ecosystem | Mature, growing | Very mature | Massive |
| CRUD productivity | Medium | High | Very high |
Rust wins where per-node performance really matters: services under sustained load, Kafka consumption at high rates, gateways multiplexing thousands of connections. In typical CRUD services where the bottleneck is the database, the gain is marginal and the learning cost doesn’t pay back.
Cases where it pays off
Four service types where Rust is adopted successfully in production:
-
Gateways and proxies. Handling 50,000 simultaneous connections with predictable memory.
-
Event processors. Kafka consumers with high throughput where Go was already showing GC pressure.
-
CPU-bound services. Image processing, voluminous log parsing, complex transformations.
-
Components embedded in other languages. Compile to a static library consumed from Python or Node.js via FFI.
For the typical "validate JSON, run 3 Postgres queries, return response" microservice, Go or Node.js remain more productive options.
Patterns that save suffering
Five tips for teams adopting Rust on the backend:
-
anyhowfor application errors,thiserrorfor library errors. Makes error propagation reasonable without boilerplate. -
tracingfor structured logs. Works well with OpenTelemetry and integrates with axum via middleware. -
towermiddleware. Uniform composition for reusable logging, auth, and rate limiting. -
Incremental build with
moldlinker. Cuts link time, and with it iteration time, on Linux. -
cargo nextest. A much faster test runner thancargo testby default.
For the cache layer over Postgres in axum services, Redis with cache-aside strategies is the most common combination. The Grafana observability stack integrates well with tracing via OTLP. Rust service containers are easier to operate with Podman in rootless environments.
Conclusion
Rust on the backend is already a serious option, not a future promise. The tokio + axum combination is stable, performant, and reasonably productive once past the initial curve. But it’s not the right answer for every service: it’s a choice with clear trade-offs to make consciously, based on real workloads and not hype.
This guide is also available in Spanish: Rust para backend: axum y tokio en servicios reales.
Sources:
- tokio: official documentation for the async runtime[1]
- axum on GitHub: the official HTTP framework repository[2]
- tower on GitHub: the middleware library axum is built on[3]
- hyper: the low-level HTTP library for Rust[4]
Frequently asked questions
Is Rust worth it for a typical CRUD service over Postgres?
Not for that kind of service: in the validate JSON, run three Postgres queries, return a response kind of service, the bottleneck is the database. Rust's gain is marginal, the learning cost does not pay back, and Go or Node.js remain more productive. Rust wins where per-node performance really matters: gateways and proxies handling 50,000 simultaneous connections, and Kafka consumers with high throughput where Go was already showing GC pressure. Also CPU-bound services and components embedded in Python or Node.js via FFI.
How long does a Rust backend take to compile, and how can I speed it up?
A medium project compiles in 30-60 seconds in debug and several minutes in release, and that is the biggest operational drawback because it slows the iteration cycle. Two tools cut the friction: the first is the mold linker for incremental builds, which cuts link time and with it iteration time on Linux. The second is cargo nextest, a much faster test runner than the default cargo test. Even so, plan for 2-3 months of reduced productivity while the team internalises ownership, lifetimes and async.
Which crates should I use for errors and logging in an axum service?
For errors, anyhow in application code and thiserror in libraries: they make propagation with ? reasonable without boilerplate. For structured logs, tracing, which works well with OpenTelemetry, integrates with axum via middleware and fits the Grafana stack through OTLP. tower middleware gives uniform composition for reusable logging, auth and rate limiting. For the cache layer over Postgres, the most common combination is Redis with cache-aside strategies.