Which Rust framework should you choose? When none is the obvious choice
You’re starting a backend in Rust, and everyone recommends a different framework. In Java or Python, the question would be all but settled in advance. In Rust, it remains open. Here is what you need to settle it for your situation, and why the decision that matters most is made beneath the framework.
RBBy Rivel BabindamanaAbout 35 minutes of reading
Written for
tech leads
devs
Ask a Java team what they start a new backend with: the answer fits in two words, Spring Boot. In most languages, one big name has become the obvious choice in the same way: Django or FastAPI in Python, ASP.NET Core in C#, Rails in Ruby. Serious alternatives exist everywhere, but they define themselves in relation to it.
Ask Rust developers the same question, and you’ll get six answers: axum, Actix Web, Rocket, Poem, Salvo, Loco. Several are excellent and run in production; none has won to the point of making the question pointless. To understand this diversity, and above all to choose, it helps to go back to basics first: what a backend does, and what a framework brings to it.
One file will be our common thread: the Cargo.toml, where a Rust project declares its dependencies. It starts out empty; each part will add a line or a decision to it, and by the end, you’ll be able to read yours as an architecture decision.
This article describes the ecosystem as it stands at the end of October 2026: versions, dates and downloads were taken from crates.io on October 28. The broad strokes will hold for a long time; version numbers, on the other hand, change quickly. I’ll update the article when the essentials change.
Part 1 · [backend]
What a backend does, and what a framework brings to it
A backend application has one main mission: receive requests, process them, and respond. These requests almost always arrive over HTTP, the protocol of the web: that is the case for a REST API, called by a browser or a mobile app.
Let’s take an example that will follow us through the whole article: my-api, a small fictional service written in Rust. It serves articles. A client asks it for article no. 42. Let’s follow its request, from arrival to response:
The clienta browser, a mobile app: it sends a request, then waits for the response.
the requestGET /articles/42the response200 OKand the article as JSON
The backendthe my-api program, written in Rust: the language frames everything else
1Async runtimemoves thousands of requests forward at once on a few threadsfoundations: tokio
2HTTP serveraccepts the connection and decodes the HTTP requestfoundations: hyper
3Middlewareprocessing shared by every request: logging, authentication…foundations: tower
4Router and extractorspicks the function that responds, and pulls from the request what it expects: the ID, the JSONframework
5Handlerthe function that responds: calls the business logic, returns the responseyour code
6Business logicthe application’s rules, the databaseyour code
1 to 4: the plumbing, the same for every application5 and 6: what makes your application yours
your codeframeworkfoundations, shared cratesstandard library: no steplanguage, the frame
The request goes through the six steps, then the response heads back to the client. The colors show who provides each step in an axum backend, the most common case; they are the same in every diagram of the article. With other frameworks, the framework also provides step 3, or even step 2: that is the criterion that matters most, and we’ll come back to it.
Steps 1 to 4 look alike from one application to the next, and nobody wants to rewrite them every time. That is the framework’s job:
Definition
A framework is a set of ready-made tools that takes care of an application’s plumbing (here, steps 1 to 4) and imposes a way to organize the rest. It doesn’t change the language: it is code written in that language, which you add to your project.
Yet in the diagram, only one step has the framework’s color. The first three are provided by foundations: libraries shared by almost every Rust framework, which the standard library does not provide. Let’s compare with Java, where the framework does everything:
In Java
Your code56controllers, services, business logic
Framework: Spring Boot1234the HTTP server (embedded Tomcat) and its threads, filters, routingplus injection, configuration, data access, security…
Separate foundationsnone: the framework ships its own
Standard librarythe JDK: collections, files, networking…
The languageJava and its virtual machine
In Rust
Your code56handlers, business logic
Framework4router and extractors: axum, Actix Web, Rocket…
Foundations123crates shared by almost every frameworktokiohypertowerplus serde for JSON, tracing for traces
Standard libraryno HTTP, no async engine
The languageRust and its compiler
your codeframeworkfoundationsstandard librarylanguage
Same layers, same colors: only the size changes. In Java, the framework handles all the plumbing, HTTP server included. In Rust, the thick layer sits below: shared foundations, on top of which the framework is just a thin layer. The numbers refer to the steps of the journey.
Where these layers live: crates
Definition
In Rust, a library is called a crate, and is published on crates.io. Cargo.toml is the file that declares the ones your project depends on. The framework is one line in it, next to the foundations: that is the common thread of this article.
What you can hand over to the framework
Not every framework does as much. The most minimalist stop at routing and reading requests; the most integrated also decide on the database, background jobs, even the user interface. Every decision it makes is one you won’t have to make, and one you can no longer make any other way.
Routing
Reading requests
Middleware
Configuration
Database
Authentication
Background jobs
User interface
All frameworksThe foundations or the framework (criterion 1)The most integrated, on top
These two ideas, what the framework replaces in the foundations and what you hand over to it above them, will become the first two criteria in part 3.
$ cargo new my-api
# [dependencies]: empty. Rust imposes nothing on you, not even an HTTP server.
[backend] · the takeaway
A backend is six steps, and a framework takes care of the plumbing. In Rust, shared foundations do most of it, and the standard library none at all. That is no accident: it is a philosophy, and a history.
The absence of one big framework in Rust is neither a delay nor an accident. It follows from choices made by the language, and from an eventful history. Together they explain why, in the journey from part 1, the foundations do most of the work.
A standard library that stops short of networking
Part 1 showed it: Rust’s standard library provides none of the steps in the journey. Compare it with Go’s, the other language where no framework has become the obvious choice:
With no dependencies at all…GoRust
Open a TCP connectionyesyes, blocking
Run concurrent codeyes, goroutinespartly: async, no engine
Be an HTTP serveryes, net/httpno
Read and write JSONyesno
Encrypt with TLSyesno
In Go, a web server comes out of the box, and the framework is optional. In Rust, even the simplest server starts with dependencies.
This is a deliberate choice: whatever goes into Rust’s standard library stays there forever, with a compatibility promise that has no end date. But HTTP keeps moving (HTTP/2, then HTTP/3), and the right way to run asynchronous code depends on the machine, from server to microcontroller. So Rust left all of that to crates. And as soon as there is a dependency, there is a choice.
Async arrived late, and its engine stayed outside
The async and await keywords have only been stable in Rust since November 2019 (version 1.39). The language defines what an asynchronous task is, but not who runs it: you need a runtime, and for years two of them fought for the spot, tokio and async-std. A library written for one didn’t always work with the other, and every framework had to place a bet.
tokio won: version 1.0 in December 2020, then widespread adoption. In February 2025, async-std was officially discontinued. Tide, the web framework built on it, hasn’t published a release since December 2021: remember that name, it will come back as a lesson.
Eleven years of history, based on crates.io release dates and project announcements.
The foundations have only been stable for a short while: hyper, the HTTP library almost everyone uses, only released its 1.0 in November 2023. Spring Boot was built on a Java platform nearly twenty years old; Rust frameworks were built while the ground was still shifting. None had time to crush the others.
No reflection, so no runtime magic
Much of Spring’s power rests on reflection: a Java program’s ability to inspect itself while it runs. At startup, Spring scans your classes, reads their annotations and injects dependencies wherever you asked for them. You annotate, and it works.
Rust has no runtime reflection: a framework cannot “discover” your code at startup. Everything is resolved at compile time, through types, traits and macros. Compare:
Java, Spring Boot
@RestControllerclassArticleController {
// injected at startup, via reflection@Autowired ArticleRepository repo;
@GetMapping("/articles/{id}")
Article get(@PathVariablelong id) {
return repo.findById(id).orElseThrow();
}
}
Rust, axum
// an ordinary function: its parameters state// what it expects, and the compiler checks itasync fnarticle(
State(db): State<Db>,
Path(id): Path<u64>,
) -> Result<Json<Article>, StatusCode> {
db.find(id).await.map(Json).ok_or(StatusCode::NOT_FOUND)
}
// … and you are the one who passes the state to the router:// Router::new().route(…).with_state(db)
On the Spring side, the framework does a huge, invisible amount of work. On the Rust side, there is no injection container: state is passed explicitly, and the function signature is the contract. If a parameter can’t be extracted from the request, the code doesn’t compile. Hence the most important idea of this part:
Elsewhere, the framework handles itIn Rust, a type does it
Dependency injectiona parameter: State<Db>
Reading and validating dataa type that rejects invalid values: Json<NewArticle>
Error handlinga return type: Result
Discovering routes at startuproutes declared in code, checked at compile time
In Rust, the type system does part of the work a framework does elsewhere. The framework no longer needs to be a platform: it can settle for being a well-typed router.
A culture of composition
The Rust ecosystem got used very early to small crates that do one thing, very well: serde for serialization, tracing for logs and traces, sqlx for the database. None of them belongs to a framework, and that is by design. A weakness, because you have to assemble things yourself; a rare strength, because you can swap one brick without moving house. The rest of the article is about figuring out which of the two matters more to you.
What this history produced: shared foundations
The result: in almost every Rust backend in production, you find the same layers, those of steps 1 to 4 of the journey. The framework sits on top, and it isn’t the thickest one. The diagram below shows them for each framework, with the colors from part 1:
shared foundationsspecific to the frameworkcross-cutting crates
Simplified. The numbers are those of the journey from part 1. Only the axum column rests entirely on the shared foundations; the more a column takes on the framework’s color, the more the framework has replaced them with its own bricks. That is what we’ll measure in part 3.
tokiothe runtime
The engine, and the de facto standard
tokio runs your asynchronous tasks: it spreads the work across all cores, handles non-blocking I/O, timers, channels. Every framework in this article runs on it, Actix Web included, which uses it in its own way: one small independent runtime per core.
The alternatives target specific cases (smol for minimalism, glommio or monoio for io_uring on Linux, embassy for microcontrollers). If you’re building a web backend and you’re hesitating, you’re not in one of those cases.
hyperthe protocol
HTTP, done once, done well
hyper implements HTTP/1 and HTTP/2, on the server side as on the client side. It’s rarely used directly: axum, tonic, Rocket, Poem, Salvo, warp and reqwest, the most widely used HTTP client, are built on it. Its 1.0, in November 2023, gave Rust’s HTTP stack a stable API for the first time. Only Actix Web has its own engine, actix-http.
towerthe contract
The contract that changes everything
This is the least known layer, and yet the most important for your decision. tower does almost nothing on its own: it defines a contract, the Service trait. A service is something that receives a request and will return, later, a response or an error.
The core of tower (simplified)
pub traitService<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
// “Are you ready to receive a request?”: backpressure is part of the contract.fnpoll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
// Handle the request: the response will come later.fncall(&mut self, req: Request) -> Self::Future;
}
The consequence is decisive: in the tower world, a middleware layer is simply a service that wraps another one (a Layer). Written once, a layer wraps any service, whatever the framework:
The same layers, written once, around two different services. This shared contract is what we’ll call the “tower family”. The tower-http crate provides a whole collection of them, ready to use: in code, it looks like this.
tower-http layers around an axum router
let app = Router::new()
.route("/articles/{id}", get(article))
.layer(CompressionLayer::new()) // tower-http: gzip, brotli…
.layer(CorsLayer::permissive()) // tower-http: CORS headers
.layer(TraceLayer::new_for_http()); // tower-http: one trace per request, via tracing// The same layers can wrap a tonic gRPC server.
That is the secret behind the rise of axum, released only in 2021: it arrived in an ecosystem that already existed, and was just waiting for a router. axum is not a framework that created its ecosystem: it is an ecosystem that found its framework.
cross-cuttingeverywhere
What follows you, whatever the framework
serde for JSON, tracing for logs and traces (and OpenTelemetry), sqlx, SeaORM or Diesel for the database, reqwest to call other services, thiserror and anyhow for errors. None of them depends on the framework. In a real project, the framework line is often one of the shortest in Cargo.toml.
Two families, and a few independents
Depending on which layers each framework reuses or reinvents, the landscape falls into two families: the tower family, which shares its middleware with the whole ecosystem, and the independents, each with its own model, sometimes very well done, but apart.
Framework
HTTP
Middleware
Coding style
Macros
axum
hyper
tower
Functions and extractors
Almost none
Loco, Cot, Leptos, Dioxus
hyper (via axum)
tower
The meta-framework’s own
Varies
Actix Web
actix-http
actix-service
Functions and extractors
For routes (optional)
Rocket
hyper
Fairings
Attribute macros
Everywhere
Poem
hyper
In-house (tower bridge)
Functions and extractors
#[handler]
Salvo
hyper
In-house
Everything is a handler
#[handler]
warp
hyper
Filters
Combinators
None
This table doesn’t look like much. Yet it holds the most structuring criterion in this article, and the least discussed. We’ll come back to it in part 3.
What it changes for you
The standard sits lower in the stack
There is no standard framework, but there are standard foundations: tokio, hyper, serde, tracing. That is where the ecosystem unified, and that is where your code’s longevity is decided.
A bad choice costs less than elsewhere
Your domain types, your serde structs, your sqlx queries don’t depend on the framework. Switching frameworks means rewriting the routes, the handlers, sometimes the middleware, not the application. Leaving Spring often means rewriting everything.
But the assembly is up to you
Configuration, authentication, the ORM, API documentation: a minimal framework lets you choose each one. That is the real question a Rust framework raises: how much of the assembly do you want to do yourself?
$ cargo add tokio --features full
$ cargo add serde --features derive
$ cargo add tracing tracing-subscriber
# four lines in [dependencies], and still no framework
[history] · the takeaway
Rust has no Spring because it doesn’t need one in the same way: types do part of the work, and the ecosystem unified lower down, in shared foundations. What remains is how to compare the frameworks built on top.
To compare tools this different, you need a grid. It has eight criteria. The first two follow directly from part 2 and draw the map of the candidates; the other six refine it. And the one people always cite first, speed, barely makes the list.
1The distance from the shared foundations
2The level of integration
3Maintenance
4API stability
5The execution model
6Compile errors
7The API contract
8The team
✕Raw speed
1The distance from the shared foundations, the criterion that matters most
The Rust ecosystem has unified beneath the framework, in tokio, hyper and tower. So a Rust framework is judged first by how much of them it keeps:
Definition
A framework’s distance from the shared foundations is everything it replaces with its own building blocks: its middleware model (step 3 of the journey), sometimes its HTTP server (step 2).
This distance comes at two prices:
Consequence 1 · the exit cost
What you’d have to rewrite the day you leave the framework, for another tool or for the foundations alone.
Consequence 2 · the lost ecosystem
Everything that speaks tower and that you no longer inherit: tower-http, tonic on the same port, lambda_http, meta-frameworks.
The day you leave comes more often than you’d think. That’s what happened with Tide:
The tool is no longer maintainedA breaking version is comingYour needs change: gRPC, serverless, a published contract
How to measure it: the middleware model
The distance can be read in a single line, at step 3 of the journey: what you have to implement to write middleware. The shared tower contract, or a homegrown model? Here is each framework’s answer:
impl tower::Layer<S>axumLocotonic
impl poem::Middleware<E>Poem
#[handler]async fn(…, ctrl: &mutFlowCtrl)Salvo
impl rocket::fairing::FairingRocket
impl actix_web::dev::Transform<S, ServiceRequest>Actix Web
The first line is the contract of the whole tower ecosystem: a layer written for one works for all. The other four are models specific to their framework, often well designed, but they only plug into that framework. Actix Web also replaces the HTTP server.
Here is what that changes for two identical projects, one written with axum, the other with Actix Web, when the time comes to leave:
axum
Your code
routeshandlersmiddlewarebusiness logic
shared contract: tower::Service
Frameworkrouter, extractors
Foundationstokiohypertower
Actix Web
Your code
routeshandlersmiddlewarebusiness logic
Actix contract: actix-service
Frameworkrouter, extractors, middleware, HTTP server
Foundationstokio, through actix-rt
to rewrite when leavingsurvives
In both cases, routes and handlers must be rewritten: every Rust framework has its own extractors. The difference lies underneath. With axum, your middleware consists of tower layers: they follow you to any framework in the family, and they also work with tonic. With Actix Web, it leaves with the framework.
The more a framework replaces the shared foundations with its own building blocks, the more it does things its own way, the less it inherits from the ecosystem, and the more leaving costs.
That’s not a flaw: it’s a trade-off. But it’s decided on day one, not on the day you leave.
2The level of integration, the second axis
Rust delivers the backend as a kit. Some frameworks give you the parts and the instructions; others, the furniture already assembled. These are the eight decisions from part 1, laid out on a line:
No point on the spectrum is better than another. On the left, freedom and control; on the right, speed and consistency.
Toward the right, you save time at the start, and you depend on someone else’s choices: Loco chose SeaORM for you, a gift if you had no opinion, a constraint if you preferred sqlx. Toward the left, you write more glue code, but that code is yours. And the two axes are independent: Loco sits far along this one, yet very close to the foundations on the first, since it’s built on axum.
The six other criteria
Once these two axes are set, the remaining question is what each framework is worth day to day. Six criteria tell you.
3
Maintenance and governance
The likelihood that the framework will still be maintained when your service turns five. In Rust, many projects rest on a handful of people: Rocket went twenty-nine months without a release before 0.5.2, Poem hasn’t published anything since July 2025, Tide stopped along with its foundation.
My rule: a project backed by an organization, with several active maintainers and regular releases, rather than a brilliant project carried by a single person.
4
API stability
A crate at 0.x can break its API with every minor release. axum is still at 0.x; Actix Web has been at version 4 since 2022, Salvo and Loco have just reached 1.0. axum’s breaking changes are rare (about one a year) and often mechanical, but across ten services, you pay for them ten times.
Plan for: one upgrade per year per service, in the schedule, and a Cargo.lock so it never catches you by surprise.
5
The execution model
With tokio in multi-threaded mode (axum, Poem, Salvo, Rocket), a task can move to another core at every .await: whatever it holds must be able to travel between threads (be Send). Actix Web, with one runtime per core, doesn’t require it for your handlers.
For a team new to Rust: it’s the compiler protecting them from a concurrency bug, but it’s one of the most common errors at the start. Explain it before it discourages them.
6
Compile errors
What decides the productivity of a team that is learning. Frameworks built on generic traits (axum, warp) produce long errors; macro-based ones (Rocket), errors that go through the macro. axum has a simple answer, #[debug_handler], which points to the faulty parameter.
The test I recommend: a route with a deliberate error, written by someone new to the framework. Time how long it takes them to understand the message.
7
The API contract
If other teams consume your API, its OpenAPI documentation is part of the product. poem-openapi derives it directly from the types; utoipa works with axum, Actix Web and Rocket; aide is designed for axum; Salvo has its own module, salvo-oapi.
My advice: decide on day one whether the specification is generated from the code or written by hand. Both are defensible; mixing them is not.
8
The team: learning, hiring, getting help
Zero To Production in Rust teaches Actix Web, Rocket’s guide is a model of its kind, axum’s repository is full of examples. A Rust backend developer almost always knows axum or Actix Web. So do coding assistants, but they mix up versions: one that suggests /:id for axum 0.8 learned from pre-2025 code.
To check: who will maintain this code in three years, and what those people already know.
What about speed?
All these frameworks are very fast. On a server that returns a string, Actix Web often gets a slight edge from its per-core runtime; on a real service, which queries a database and serializes JSON, these gaps get lost in everything else. So raw speed joins the things that shouldn’t weigh in:
“Hello world” benchmarks. Rust frameworks long fought over the top of the TechEmpower rankings, archived in March 2026. They measured servers that return a string. Yours will be waiting on a database: that’s what will decide.
GitHub stars. They measure the enthusiasm of a moment, not the health of a project. The release pace and the number of maintainers say much more.
The beauty of “hello world.” Every framework is elegant in ten lines. The question is what they become at ten thousand, with authentication, errors and tests.
The first tutorial you read. People often stick with the framework they learned Rust with. It’s human, and it’s a very bad reason to commit a team to it.
If performance really matters, there’s only one method: a prototype of the busiest endpoint, in two frameworks, with the real database and a realistic load. One day of work, worth more than a hundred benchmarks read online.
$ cargo tree --depth 1
# what each line of the file pulls in with it: the distance, as Cargo sees it
[criteria] · the takeaway
Eight criteria, two of which draw the map, and speed put back in its place. We can finally ask the right question.
With these criteria in hand, we can reframe the starting question.
The question everyone asksWhat’s the best Rust web framework?
The question to ask yourselfWhat does my team need on top of the shared foundations, and how much will leaving cost if I change my mind?
The first question has no answer: comparisons line up benchmarks and conclude with “it depends,” without ever saying on what. The second has one, different for each project. I’ll also say which one I like best, and why.
Each half of the right question is weighed with the criteria from part 3:
First half · what you gainWhat does my team need on top of the shared foundations?
It’s weighed with criteria 2 to 8:
2integration
3maintenance
4stability
5execution
6errors
7contract
8team
Second half · what you’ll payHow much will leaving cost if I change my mind?
It’s read with criterion 1:
1the distance from the shared foundations
The rule that follows: pick the tool closest to the shared foundations that covers your needs.
To apply it, seven concrete questions, to ask as a team before writing the first route. The answers almost always point to the right candidate:
What don’t the foundations give me?criteria 2 to 8Make a list, in writing. If it’s short, axum and a few crates may be enough.
How many decisions do we want to hand over to the framework?criterion 2Configuration, database, background jobs, emails: a team in a hurry gains by delegating them, a team with opinions gains by keeping them.
Will we need gRPC, serverless, middleware we won’t write ourselves?criterion 1If the answer is “maybe,” the tower family is insurance that costs almost nothing.
Who consumes this API?criterion 7Your own applications, or partners? If a published contract is a deliverable, it may choose the framework for you.
Who will maintain this code?criteria 5, 6 and 8Not today’s team: the one three years from now. What it knows about Rust, and what it will easily find online.
Who maintains the tool, and how often does it break?criteria 3 and 4Date of the last release, number of active maintainers, version number: 0.x or 1.0.
Does my business code import the framework?criterion 1The answer must be no, whatever tool you choose. It’s the only rule that applies to every candidate.
# The missing line, at last. But which one?# axum = ? actix-web = ? rocket = ? poem = ? salvo = ? loco-rs = ?
[question] · the takeaway
The question is asked, the grid is ready. Let’s see how each candidate answers it.
Six candidates, ordered from closest to the shared foundations to farthest. For each one: an overview, where it stands on the eight criteria, the situations where it fits best, its challenges and risks, and my verdict.
The first two criteria, for the six candidates. From left to right, a framework replaces more and more of the foundations with its own; from bottom to top, it decides more and more things for you. Positions are estimated, not measured. Click a name to go to its card.
Each card includes the same endpoint, collapsed: GET /articles/{id}, which reads an article from a shared database and returns JSON, or a 404. The Db type is the same everywhere: only the framework changes.
axum
0.8.9 · the tokio teamMy favoritelow distance
The smallest possible framework on top of tower. It reinvents nothing: it brings the router and the extractors, step 4 of the journey, and leaves everything else to the shared foundations. Released only in 2021, it arrived in an ecosystem that was just waiting for a router.
One thing to be clear about: it’s my favorite. I used Actix Web for a long time; I like it, and I still recommend it, especially for its maturity. But axum won my heart. I don’t love axum because it does a lot. I love it because it does exactly what the foundations are missing, and nothing else: it’s the smallest distance you can take from them while still gaining something real. Five reasons:
It gets out of the way. A handler is an ordinary async function, with no macro and no hidden lifecycle: a new hire reads Rust, not “axum.”
The types are the contract. The signature says what the handler expects and what it returns; the compiler checks the plumbing, and I keep my attention for the business logic.
It speaks tower, so it talks to everything.tonic on the same port, tower-http, AWS Lambda or Cloudflare when needed.
It’s maintained by the people who maintain the foundations. When tokio, hyper and tower evolve, it evolves with them, not six months later.
It grows with the project.Loco, Cot, Leptos and Dioxus are built on it: you don’t leave it, you move up a floor.
On the eight criteria
1 · Distancelow: tower end to end
2 · Integrationa kit: everything to choose
3 · Maintenancethe tokio team
4 · Stability0.x: one breaking release a year
5 · Executionmulti-threaded: Send tasks
6 · Errorsobscure, except #[debug_handler]
7 · Contractutoipa or aide, on the side
8 · Teamthe most widespread
Where it fits
a REST API or a microservice built to last;
a gRPC backend with a few REST routes, tonic on the same port;
a serverless function: lambda_http and workers-rs run an axum router;
the server of a Leptos or Dioxus fullstack application.
Challenges and risks
still at 0.x: 0.7 broke things to move to hyper 1, 0.8 for the path syntax (/:id became /{id}); 0.9 is in the works, with no date;
when a handler doesn’t respect the contract, the compile error is obscure;
configuration, database, authentication, documentation: everything is yours to choose;
tasks must be Send, a constraint that surprises people at first.
Verdict
My default starting point, for an API as well as for a microservice. What it costs me, I make up for: the yearly version upgrade is planned like any normal task, #[debug_handler] is enabled in development, and an internal project template sets configuration, errors and the database once and for all. My own “framework,” in short, in a Cargo.toml.
tonic0.14 · the gRPC neighborA tower service: an axum router can hand gRPC requests over to it on the same port. It joined the gRPC project in May 2026.
LeptosDioxusfullstackThe interface in Rust too, compiled to WebAssembly, with “server functions” called from the browser. The server is axum; Dioxus also targets mobile and desktop.
No macro on the handler. The return type is the contract: JSON, or an error code. The server is tokio at work, in plain sight, with no middleman.
Loco
1.2 · the Rails of Rustlow distance
An application framework built on axum: conventions, generators and batteries. It describes itself as the one-person framework. It replaces none of the foundations; it adds everything axum leaves you to choose: models and migrations (SeaORM), background jobs, email, authentication.
a complete product (pages, back office, jobs, email) carried by a small team;
an MVP that has to ship fast;
a team coming from Rails or Django;
a team new to Rust that would rather have decisions made for it.
Challenges and risks
choices already made: if you don’t like SeaORM, you fight the framework;
more dependencies, so longer compile times and more code to audit;
conventions to learn, on top of Rust.
Verdict
The best shortcut in the ecosystem when you’re building a whole product with few hands. And a shortcut with no dead end: the day a convention gets in the way, you go down a floor, into axum.
Cot0.7 · the bet on a Django for RustAn ORM that derives tables and migrations from your types, and a ready-to-use admin panel. Also built on axum, still young.
The code: GET /articles/{id} with Loco… without writing it
# install the tool, then create the project (it asks a few questions: database, app type)
cargo install loco
loco new
# generate the model, the migration, the controller and its routes
cargo loco generate scaffold article title:string! content:text --api
cargo loco start
Not a single line of handler code to write for this first endpoint: the generator creates the read, plus the list, create, update and delete. The generated controller is an ordinary axum handler, which you then edit by hand.
Poem
3.1 · the contract derived from the typesmoderate distance
Close to axum in spirit, with its own middleware (and a bridge to tower), and one centerpiece: poem-openapi, which makes your Rust types the single source of the OpenAPI contract. The specification and the Swagger UI are generated from the code, so they’re never out of sync.
1 · Distancemoderate: its own middleware, a bridge
2 · IntegrationOpenAPI, gRPC, Lambda as modules
3 · Maintenancenothing since July 2025
4 · Stabilityv3
5 · Executionmulti-threaded: Send
6 · Errorsthrough the #[handler] macro
7 · Contractpoem-openapi: the types
8 · Teama small community
Where it fits
an API whose OpenAPI contract is the product;
services that want gRPC, AWS Lambda, or even an MCP server for AI agents, as official modules;
a team coming from axum: the API is quick to pick up.
Challenges and risks
no release since July 2025; Poem 4 is announced in the repository, but not published;
a smaller community, so fewer ready-made answers;
its native middleware only plugs into Poem itself.
Verdict
The most elegant option for an API whose contract is the deliverable. But until Poem 4 is out, I prefer axum with utoipa, for longevity, accepting a few more annotations. Worth reconsidering the day the project starts releasing again.
Almost the same code as axum, with a #[handler] macro and the old path syntax (:id). The difference would show with poem-openapi: the same types would also produce the API documentation.
Salvo
1.0 · batteries includedhigh distance
The most “batteries included” of the HTTP frameworks. A single mental model, where everything is a handler, middleware included, and a lot provided out of the box: HTTP/3 (via quinn), automatic TLS certificates (ACME), OpenAPI, proxy, rate limiting.
a standalone HTTP service that wants everything in a single dependency, from automatic TLS to HTTP/3;
a team comfortable with Rust, ready to follow a lower-profile project.
Challenges and risks
a hundred or so 0.x releases before 1.0: an API that has moved a lot;
outside the tower family: its middleware only works with Salvo;
less familiar to the developers you’ll hire.
Verdict
The September 2026 1.0 makes it a defensible choice for a standalone service. I’d pick it when its batteries really replace several crates, while keeping a close eye on the pace of its first 1.x releases.
The code: GET /articles/{id} with Salvo 1
#[handler]async fnarticle(req: &mut Request, depot: &mut Depot) -> Result<Json<Article>, StatusError> {
let id: u64 = req.param("id").ok_or_else(StatusError::bad_request)?;
let db = depot.obtain::<Arc<Db>>().map_err(|_| StatusError::internal_server_error())?;
db.find(id).await.map(Json).ok_or_else(StatusError::not_found)
}
#[tokio::main]async fnmain() {
let db = Arc::new(Db::connect().await);
let router = Router::new()
.hoop(affix_state::inject(db))
.push(Router::with_path("articles/{id}").get(article));
let acceptor = TcpListener::new("0.0.0.0:3000").bind().await;
Server::new(acceptor).serve(router).await;
}
No extractors here: the handler receives the request and a “depot” (Depot) where middleware stores state. The middleware that injects the database into it, affix_state::inject, is itself a handler, attached with hoop: everything is a handler.
Rocket
0.5.2 · the nicest to writehigh distance
Macros everywhere, strong conventions, ergonomics designed like a product: the framework that showed Rust could be as comfortable as Rails or Flask. Its request guards turn authentication into a parameter type, checked before the handler is even entered.
3 · Maintenance29 months without a release, before 0.5.2
4 · Stability0.x
5 · Executionmulti-threaded: Send
6 · Errorsthey go through the macros
7 · Contractutoipa, on the side
8 · Teamthe easiest to learn
Where it fits
learning or teaching web development in Rust, with one of the best official guides in the ecosystem;
a prototype, or an internal tool that will stay small;
when the joy of writing matters more than longevity.
Challenges and risks
the pace: 0.5.1 in May 2024, 0.5.2 in October 2026, for a project carried mostly by a single person; community forks have published their own 0.6 pre-releases;
outside the tower family: its fairings are a world of their own;
macro magic has a price: less direct errors, heavier compilation.
Verdict
The best for making people love web development in Rust. For a product that has to last, I’d go back to axum. 0.5.2, released a few days ago, is a sign of life worth watching closely.
The shortest of the six. None becomes a 404 on its own, the port comes from the configuration (Rocket.toml), and the #[launch] macro writes main for you. Everything is concise, and part of what happens is hidden.
Actix Web
4.18 · the veteranvery high distance
A complete, standalone framework, designed for performance. It replaces steps 2 and 3 of the journey with its own, with its own HTTP engine (actix-http) and its own middleware model, and uses tokio in its own way: a small independent runtime per core.
1 · Distancevery high: its own server, its own middleware
2 · Integrationsessions, CORS, identity as modules
3 · Maintenancevery active
4 · Stabilityv4 since 2022
5 · Executionone runtime per core: no Send
6 · Errorsno dedicated tool
7 · Contractutoipa, on the side
8 · Teamvery well known, Zero To Production
Where it fits
a service where every microsecond counts: gateway, proxy, real-time advertising, game server, backed by measurements;
a project that wants a mature base and a stable API, with no yearly version upgrade;
a team that already knows it well, or existing code;
a service with no gRPC in sight.
Challenges and risks
outside the tower family: more verbose middleware, and tower-http’s can’t be used as is;
gRPC lives alongside, on another server, not inside;
an eventful history: in January 2020, its creator left the project after a controversy over its use of unsafe code, before the community took it over. It’s a reminder of what the “bus factor” is.
Verdict
I used it for a long time, and I still recommend it, first of all for its maturity: stable since 2022, very active, very fast, and taught in Zero To Production in Rust. A team that knows it well has no reason to switch just to follow the trend. I prefer axum when gRPC, serverless or tower-http middleware are on the horizon; and if throughput is what makes you choose it, measure the gap under a real load: it’s often smaller than expected.
The route is a macro, state goes through web::Data. Notice the closure passed to HttpServer::new: it’s called once per core, to build one application per worker. That’s the “one runtime per core” model, visible in the code.
The others, worth knowing
One to watchPavex
By Luca Palmieri, the author of Zero To Production. Dependency injection resolved at compile time, and errors designed for humans. A strong idea, but a 0.2 release that hasn’t moved since November 2025: not for today’s production.
Fadingwarp
The elegance of filters, from the author of hyper. Still maintained (0.4 in 2025), but the ecosystem has overtaken it: its ideas now live on in axum.
The lessonTide
Built on async-std, with no release since December 2021. A good framework, taken down by its foundation. Before you look at a framework, look at what it rests on.
Before adopting a tool outside this list, check three things: the date of its last release, the number of people who merge contributions, and what you have to implement to write a middleware layer.
Downloads lie a little
That leaves the question everyone asks: which one is used the most? crates.io publishes each crate’s download count over the last ninety days. The result looks clear-cut, at first glance:
axum
Actix Web
warp
Rocket
Salvo
Poem
Loco
axum − tonic
Downloads over the last ninety days, taken from crates.io on October 28, 2026. A download is not a project: every continuous integration build counts as one. Hatched: axum, minus all of tonic’s downloads.
These figures measure the ecosystem more than the choice. tonic, the gRPC framework, bundles axum by default, and accounts for 102.7 million downloads on its own; the meta-frameworks bundle it too. Take out all of tonic, which overstates the correction, and about 41 million remain, more than three times Actix Web. The gap is real, but smaller than it looks. Above all, it says that axum has become infrastructure, not just a framework.
$ cargo add axum
# My starting point, when nothing tips the balance. Not always the right one.
[frameworks] · the takeaway
Six candidates, six distances, and none that wins everywhere. To find yours, over to the compass.
The cards start from the candidates; the compass goes the other way. It starts from your project, asks four questions and suggests a starting point: a framework, an alternative, and the [dependencies] block of your Cargo.toml. It doesn’t replace the reasoning of the previous parts: it sums it up.
Four questions, one starting point
This is my reading grid, not the truth. Any of the six candidates can come out of it: it all depends on your project and your team.
Suggested starting point
Answer the questions alongside.
Play with the answers: the framework line changes from one case to the next, the foundations almost never do. That’s exactly what part 2 said.
$ cargo add …
# The line the compass gave you. The foundations, for their part, don’t move.
[compass] · the takeaway
The compass gives you a starting point. All that’s left is to remember the essentials.
If you were to keep only one page of this article, it would be this one.
In Rust, the foundations do most of the work
The runtime, the HTTP server, the middleware: tokio, hyper and tower provide them to almost everyone. The framework is just a thin layer on top.
The criterion that matters most: distance
The more a framework replaces these foundations with its own, the more it does things its own way, the less it inherits from the ecosystem, and the more leaving it costs. Its middleware model tells you so in one line.
The smallest distance that is enough for you
List what the foundations don’t give you, choose what you want to hand over to the framework, take the closest tool that covers all of it, and keep your business logic sheltered.
The file, once filled in
Here, concretely, is my default starting point. It’s the Cargo.toml of my-api, empty at the start of the article, once filled in:
my-api/Cargo.toml, my starting point (October 2026)
[dependencies]# The foundations (part 2): they almost never change.
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# The framework (part 5): one line, and it can be replaced.
axum = "0.8"
tower-http = { version = "0.7", features = ["trace", "cors", "compression-gzip"] }
# My building blocks (part 3): what an integrated framework would have chosen for me.
sqlx = { version = "0.9", features = ["runtime-tokio", "postgres"] }
utoipa = "6"
thiserror = "2"
Look where the framework is: one line out of nine. The rest are foundations you would keep with any other choice, and building blocks you chose yourself. That’s why none is the obvious choice: the real choice happens beneath the framework. In Rust, your framework isn’t one line of this file. It’s the whole file.
What I’m watching
axum 0.9, in the works: what it will break, and whether it paves the way for a 1.0.
Rocket’s revival: will 0.5.2 be followed by a regular pace?
Poem 4, announced: if it ships, poem-openapi becomes a top-tier choice again.
Pavex: will compiled dependency injection keep its promises at scale?
gRPC in Rust: tonic joined the gRPC project in May 2026, and an official crate is set to succeed it.
Your turn
I’ve only given my reading, and my preferences. I’d love to hear yours.
Have you ever had to leave a Rust framework? What did you have to rewrite?
Is your middleware made of tower layers, or specific to your framework? Did you choose it knowing that?
Have you ever seen the framework be the real performance bottleneck, with a profile to prove it?
Code first or contract first: which one holds up better over time for you?
Has an integrated framework like Loco, Salvo or Rocket saved you time, or cost you time, in the long run?
And you, what’s your favorite, and in which cases do you not pick it?
To reply, write to me by email or on LinkedIn. I’m always happy to reply, as my schedule allows, sometimes a little late, but I always reply.
In Rust, you don’t marry a framework the way you marry a platform. You inherit foundations, then you choose a style. Your Cargo.toml tells the story of that choice: read it as an architecture decision.
RBRivel Babindamana Engineer, October 2026
PS: if you maintain one of the frameworks in this article and I’ve misunderstood you, write to me. I’ll gladly correct it.