Tech · Backend · Rust · October 2026

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.

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/42 the response200 OKand the article as JSON
The backendthe my-api program, written in Rust: the language frames everything else
  1. 1Async runtimemoves thousands of requests forward at once on a few threadsfoundations: tokio
  2. 2HTTP serveraccepts the connection and decodes the HTTP requestfoundations: hyper
  3. 3Middlewareprocessing shared by every request: logging, authentication…foundations: tower
  4. 4Router and extractorspicks the function that responds, and pulls from the request what it expects: the ID, the JSONframework
  5. 5Handlerthe function that responds: calls the business logic, returns the responseyour code
  6. 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:

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.

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.

[history] · Philosophy and history
Part 2 · [history]

Rust’s philosophy and 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.

Timeline from 2015 to 2026. At the top, the frameworks: Rocket 0.1 in late 2016, Actix Web in 2017, warp in 2018, the Actix crisis in January 2020, axum 0.1 in 2021, Actix Web 4 in 2022, Rocket 0.5 in late 2023, axum 0.8 in January 2025, then in 2026 Loco 1.0, Salvo 1.0 and Rocket 0.5.2. At the bottom, the foundations: Rust 1.0 in 2015, stable async/await in late 2019, tokio 1.0 in late 2020, hyper 1.0 in late 2023, the end of async-std in early 2025, and tonic joining the gRPC project in 2026. FRAMEWORKS FOUNDATIONS 2015201620172018 2019202020212022 2023202420252026 Rocket 0.1 Actix Web warp axum 0.1 Actix Web 4 Rocket 0.5 axum 0.8 Loco 1.0, Salvo 1.0 Rocket 0.5.2 the Actix crisis Rust 1.0 stable async/await tokio 1.0 hyper 1.0 async-std ends tonic joins gRPC
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
@RestController
class ArticleController {
    // injected at startup, via reflection
    @Autowired ArticleRepository repo;

    @GetMapping("/articles/{id}")
    Article get(@PathVariable long id) {
        return repo.findById(id).orElseThrow();
    }
}
Rust, axum
// an ordinary function: its parameters state
// what it expects, and the compiler checks it
async fn article(
    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:

The layers of a Rust backend, from bottom to top, with the step numbers from a request’s journey. Step 1, the runtime: tokio, a foundation shared by all. Step 2, the HTTP protocol: hyper, a shared foundation, for axum, Poem, Salvo and Rocket; Actix Web has its own engine, actix-http. Step 3, the middleware model: tower, a shared foundation, for axum and tonic; Poem and Salvo have their in-house middleware, Rocket its fairings, Actix Web actix-service, all specific to the framework. Step 4, the frameworks themselves. Above axum only, the meta-frameworks: Loco, Cot, Leptos, Dioxus. A cross-cutting band is a reminder that serde, tracing, sqlx and reqwest work whatever the framework. Abovemeta-frameworks 4 Frameworkrouter, extractors 3 Middlewarewhat wraps 2 HTTPthe protocol 1 Runtimethe engine Everywherecross-cutting crates Loco · Cot · Leptos · Dioxus built on axum axum and tonic, its gRPC neighbor Poem · Salvo more integrated Rocket macros and conventions Actix Web a world of its own tower · tower-http a shared contract in-house tower bridge in Poem fairings Rocket’s model actix-service its own Service trait hyper HTTP/1 and HTTP/2, on the server side as on the client side (reqwest) actix-http its own engine tokio for everyone; Actix Web uses it through actix-rt, with one runtime per core serde · tracing · sqlx, SeaORM, Diesel · reqwest independent of the framework: they follow you if you switch
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 trait Service<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.
    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;

    // Handle the request: the response will come later.
    fn call(&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:

Two services wrapped by the same layers. On the left, an axum router; on the right, a tonic gRPC server. Around each, from outside to inside: TraceLayer, which traces each request, CorsLayer, which handles CORS headers, and CompressionLayer, which compresses responses. They are the same layers, written only once. TraceLayerone trace per request CorsLayerCORS headers CompressionLayergzip, brotli axum router your REST handlers TraceLayerthe same CorsLayerthe same CompressionLayerthe same tonic server your gRPC services A request goes through the layers from outside to inside, and the response comes back out the other way.
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.

FrameworkHTTPMiddlewareCoding styleMacros
axumhypertowerFunctions and extractorsAlmost none
Loco, Cot, Leptos, Dioxushyper (via axum)towerThe meta-framework’s ownVaries
Actix Webactix-httpactix-serviceFunctions and extractorsFor routes (optional)
RockethyperFairingsAttribute macrosEverywhere
PoemhyperIn-house (tower bridge)Functions and extractors#[handler]
SalvohyperIn-houseEverything is a handler#[handler]
warphyperFiltersCombinatorsNone

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.

[criteria] · The criteria
Part 3 · [criteria]

The criteria for choosing a Rust framework

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.

  1. 1The distance from the shared foundations
  2. 2The level of integration
  3. 3Maintenance
  4. 4API stability
  5. 5The execution model
  6. 6Compile errors
  7. 7The API contract
  8. 8The team
  9. ✕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 maintained A breaking version is coming Your 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: &mut FlowCtrl)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:

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:

A spectrum, from flat-pack kit to assembled furniture. On the left, hyper alone: you decide everything. Then axum, which provides routing and extractors; Poem and Salvo, which add OpenAPI, TLS and other features; Rocket, with its conventions and macros; Loco and Cot, with an ORM, background jobs, emails and generators; and on the right, Leptos and Dioxus, which also provide the interface. The further right you go, the more the framework decides for you. what you assemble yourself what the framework decides for you hyper aloneaxumPoem · Salvo RocketLoco · CotLeptos · Dioxus you write everythingrouting, extractors+ OpenAPI, TLS… + conventions, macros+ ORM, jobs, emails+ the interface the kit the assembled furniture
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.

[question] · The right question
Part 4 · [question]

The right question

With these criteria in hand, we can reframe the starting question.

The question everyone asks What’s the best Rust web framework?
The question to ask yourself What 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 gain What 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 pay How 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:

  1. 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.
  2. 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.
  3. 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.
  4. Who consumes this API?criterion 7Your own applications, or partners? If a published contract is a deliverable, it may choose the framework for you.
  5. 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.
  6. 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.
  7. 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.

[frameworks] · The candidates
Part 5 · [frameworks]

The candidates, one by one

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 map of the candidates, along the first two criteria. Horizontally, the distance from the shared foundations, from zero to very high; vertically, the level of integration, from flat-pack kit to assembled furniture. hyper alone: zero distance, at the very bottom. axum: low distance, lightly integrated. Loco: low distance, highly integrated. Poem: moderate distance, medium integration. Rocket and Salvo: high distance, medium to strong integration. Actix Web: very high distance, medium integration. A band covers the zero and low distances: the tower family. the tower family you inherit the whole ecosystem zerolowmoderatehighvery high distance from the shared foundations (criterion 1) → the kit assembled furniture level of integration (criterion 2) → hyper alone axum Loco Poem Salvo Rocket Actix Web
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.

async fn(State<Db>, Path<u64>) -> Result<Json<Article>, StatusCode>

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:

  1. 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.”
  2. 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.
  3. It speaks tower, so it talks to everything. tonic on the same port, tower-http, AWS Lambda or Cloudflare when needed.
  4. It’s maintained by the people who maintain the foundations. When tokio, hyper and tower evolve, it evolves with them, not six months later.
  5. 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.
The code: GET /articles/{id} with axum 0.8
async fn article(
    State(db): State<Arc<Db>>,
    Path(id): Path<u64>,
) -> Result<Json<Article>, StatusCode> {
    db.find(id).await.map(Json).ok_or(StatusCode::NOT_FOUND)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/articles/{id}", get(article))
        .with_state(Arc::new(Db::connect().await));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

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.

async fn(State<AppContext>, Path<i32>) -> Result<Response>

On the eight criteria

1 · Distancelow: axum underneath
2 · IntegrationORM, jobs, email, generators
3 · Maintenanceone release a month
4 · Stability1.x since July 2026
5 · Executionaxum’s: Send
6 · Errorsaxum’s, plus the conventions
7 · Contractaxum’s crates
8 · Teamconventions to learn
Where it fits
  • 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.

#[handler] async fn(Data<&Db>, Path<u64>) -> poem::Result<Json<Article>>

On the eight criteria

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.

The code: GET /articles/{id} with Poem 3
#[handler]
async fn article(Data(db): Data<&Arc<Db>>, Path(id): Path<u64>) -> poem::Result<Json<Article>> {
    db.find(id).await.map(Json).ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = Route::new()
        .at("/articles/:id", get(article))
        .data(Arc::new(Db::connect().await));
    Server::new(TcpListener::bind("0.0.0.0:3000")).run(app).await
}

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.

#[handler] async fn(req: &mut Request, depot: &mut Depot, res: &mut Response)

On the eight criteria

1 · Distancehigh: its own middleware
2 · IntegrationHTTP/3, TLS, OpenAPI, proxy
3 · Maintenancevery active
4 · Stability1.x since September 2026
5 · Executionmulti-threaded: Send
6 · Errorsthrough the #[handler] macro
7 · Contractsalvo-oapi, built in
8 · Teamlittle known
Where it fits
  • 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 fn article(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 fn main() {
    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.

#[get("/articles/<id>")] async fn(&State<Db>, u64) -> Option<Json<Article>>

On the eight criteria

1 · Distancehigh: fairings, its own lifecycle
2 · Integrationconventions, configuration, guards
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 code: GET /articles/<id> with Rocket 0.5
#[get("/articles/<id>")]
async fn article(db: &State<Db>, id: u64) -> Option<Json<Article>> {
    db.find(id).await.map(Json)
}

#[launch]
async fn rocket() -> _ {
    rocket::build()
        .manage(Db::connect().await)
        .mount("/", routes![article])
}

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.

#[get("/articles/{id}")] async fn(web::Data<Db>, web::Path<u64>) -> impl Responder

On the eight criteria

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 code: GET /articles/{id} with Actix Web 4
#[get("/articles/{id}")]
async fn article(db: web::Data<Db>, id: web::Path<u64>) -> impl Responder {
    match db.find(id.into_inner()).await {
        Some(a) => HttpResponse::Ok().json(a),
        None => HttpResponse::NotFound().finish(),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let db = web::Data::new(Db::connect().await);
    HttpServer::new(move || App::new().app_data(db.clone()).service(article))
        .bind(("0.0.0.0", 3000))?
        .run()
        .await
}

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:

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.

[compass] · The compass
Part 6 · [compass]

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.

1. You’re building…
2. Your team and Rust?
3. It already knows…
4. What matters most?
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.

    [summary] · What to remember
    Part 7 · [summary]

    What to remember

    If you were to keep only one page of this article, it would be this one.

    1. 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.

    2. 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.

    3. 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.

    Final word
    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.
    Rivel 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.