Short version for the impatient: I default to Axum for new Rust services, reach for Actix when I need raw throughput on a single box, and quietly love Rocket on side projects. If you only want the answer, you can stop reading. If you want to know why and what trade-offs each one is actually making, the rest of this post is for you.
I got asked the “which Rust web framework should I use” question three times in the last month. Two of the askers were former Node developers who had been hurt by Express’s micro-package situation. One was a Go developer evaluating Rust for an internal API gateway. Same answer, different reasoning each time.
The four questions I ask before opening Cargo.toml
Before I pick anything, I write four answers in a notepad. They take longer than picking a framework but they save me from the cycle of starting in one and migrating six weeks later.
The first is, how strict is my latency budget. A p99 of 50ms for an internal API is a different problem from a 500ms p99 on a CRUD app for a hundred users. Both are fine. The framework matters less than people think for the first one and more than people think for the second.
The second is, am I writing handlers I’ll change weekly or a service that mostly runs forever. Frequent change rewards clear ergonomics. Stable services reward minimal magic.
The third is, do I need first-class WebSocket or streaming. Some frameworks treat these as first-class. Others bolt them on with adequate but awkward APIs.
The fourth is, who else on my team is writing Rust. If it’s just me, I optimise for joy. If there are three others, I optimise for the framework with the best Axum-style documentation, because they’ll be the ones reading docs at 11pm when something breaks.
Axum, my default
Axum is the Tokio team’s web framework and it shows. It uses Tower middleware, so all the ecosystem work you’ve done with tower-http, tower-otel, or compression layers carries over without thinking. The handler signature is plain async functions with extractors as arguments, which composes well and reads cleanly.
Here’s the smallest useful service I’d ship in Axum:
use axum::{routing::get, Json, Router};
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();
}
That’s the whole thing. Add a .with_state(...) for shared state, a tower middleware for logging, and you have something I’d happily put behind a load balancer. The current Axum README walks through the same shape with more decoration.
Where Axum has bitten me: the extractor model can produce error messages that are hard to read when you’ve nested a generic too far. The fix has always been to break the handler into smaller functions, which is good practice anyway, but the compile error is intimidating the first time you see it. I cover one specific Axum error-handling pattern I keep around in my notes on the axum error handling split that stopped opaque 500s.
Actix when I’m pushing throughput
Actix-web has been at or near the top of the TechEmpower benchmarks for years. That’s not a reason to choose it. But it is a real signal that the framework is tuned for the request-response loop in a way the others aren’t.
The handler model uses extractors too, but the framework owns its own runtime, and that’s the trade-off you’re buying. It means you stop sharing your Tokio executor with the rest of your async code and start running an Actix runtime instead. For a service that is the web server, that’s fine. For a process that also runs a queue consumer and a gRPC client, it’s a wart.
use actix_web::{get, web, App, HttpServer, Responder};
use serde::Serialize;
#[derive(Serialize)]
struct Health { status: &'static str }
#[get("/health")]
async fn health() -> impl Responder {
web::Json(Health { status: "ok" })
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(health))
.bind(("0.0.0.0", 3000))?
.run()
.await
}
If I’m writing a service that does one thing very fast (a redirect service, a hot lookup endpoint, a feature flag server) Actix is a fine default. I would not start a generalist API in it today.
Rocket on side projects, deliberately
Rocket is the framework I have the most affection for, and the one I’d think twice about for a serious production service. The macro-based routing reads like a tutorial:
#[macro_use] extern crate rocket;
use rocket::serde::{json::Json, Serialize};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Health { status: &'static str }
#[get("/health")]
fn health() -> Json<Health> {
Json(Health { status: "ok" })
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![health])
}
The ergonomics are gorgeous. The compile errors are friendlier than Axum’s. The fairings, guards, and form handling all feel cohesive in a way that the more component-based frameworks don’t.
The reason I keep it for side projects: the Rocket release cadence is slower than Axum’s, the Tower ecosystem doesn’t compose with it the same way, and finding senior contributors who know it well is harder. For a side project I’m building solo, that’s all fine and the joy of the API more than makes up for it. For a service my team is going to be on call for, I want the boring choice.
What I’d do this week
If you’re starting a new Rust web service this week, write your four answers first. Then pick Axum unless one of them gives you a strong reason not to. The boring default is right more often than any framework comparison would have you believe.
If you’ve been on Actix for a while and the throughput requirements have relaxed, an Axum port is usually less work than people expect (the handler bodies often translate one-to-one once you swap extractors). I covered an adjacent decision in Rust vs Go in 2026: how I actually choose if you’re still up at the language choice rather than the framework choice.
And if you want a second opinion on a real service before you commit to any of this, I sometimes take on Rust consulting engagements. Even when I disagree with the call, I’ll tell you why on the same page. That tends to be more useful than a benchmark chart.