Rust’s HTTP framework landscape is surprisingly nuanced, and the "better" choice between Actix-web and Axum isn’t about raw performance, but about how well each aligns with your project’s specific needs and your team’s familiarity.

Let’s see Axum in action. Imagine a simple service that tracks the popularity of different fruits.

use axum::{
    routing::{get, post},
    Router,
    extract::{State, Path},
    http::StatusCode,
    response::Json,
};
use std::collections::HashMap;
use tokio::sync::Mutex;
use serde::{Serialize, Deserialize};

#[derive(Clone)]
struct AppState {
    fruit_counts: Mutex<HashMap<String, u32>>,
}

#[tokio::main]
async fn main() {
    let app_state = AppState {
        fruit_counts: Mutex::new(HashMap::new()),
    };

    let app = Router::new()
        .route("/fruit/:name", get(get_fruit_count).post(add_fruit))
        .with_state(app_state);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    println!("Listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

async fn get_fruit_count(
    State(state): State<AppState>,
    Path(fruit_name): Path<String>,
) -> Result<Json<u32>, StatusCode> {
    let counts = state.fruit_counts.lock().await;
    match counts.get(&fruit_name) {
        Some(count) => Ok(Json(*count)),
        None => Err(StatusCode::NOT_FOUND),
    }
}

#[derive(Deserialize)]
struct FruitPayload {
    name: String,
}

async fn add_fruit(
    State(state): State<AppState>,
    Json(payload): Json<FruitPayload>,
) -> StatusCode {
    let mut counts = state.fruit_counts.lock().await;
    let count = counts.entry(payload.name).or_insert(0);
    *count += 1;
    StatusCode::CREATED
}

This code defines a stateful application where a HashMap stores fruit counts, protected by a tokio::sync::Mutex. The Router maps GET requests to /fruit/:name to get_fruit_count, which retrieves the count, and POST requests to the same path to add_fruit, which increments the count. The AppState is shared across all requests.

The core problem these frameworks solve is efficiently handling concurrent network requests in a performant, type-safe manner. Rust’s ownership and borrowing rules, while powerful, can make traditional asynchronous web server development complex. Both Actix-web and Axum provide abstractions to manage this complexity, but they do so with different philosophies.

Actix-web is built on the actor model, leveraging Rust’s actix crate. Each request can be thought of as a message sent to an actor, which processes it and sends back a response. This model is inherently concurrent and provides strong isolation between components. Its architecture has historically led to very high performance benchmarks, often topping lists for raw throughput.

Axum, on the other hand, is built by the Tokio team, the de facto standard for asynchronous Rust. It embraces Tokio’s ecosystem and its async/await paradigm directly. Instead of actors, Axum uses a system of extractors and middleware that compose nicely with Tokio’s runtime. This often leads to a more intuitive mental model for developers already familiar with async/await and Tokio’s patterns. The extractors mechanism, like State and Path in the example, are a prime example of this: they allow you to declaratively pull data needed for your handler directly from the request.

Here’s a deeper dive into their internal mechanics. Actix-web’s actor system means that when a request comes in, it’s processed by an actor. If that actor needs to perform an I/O operation (like a database query), it sends a message to another actor responsible for that I/O and then yields control, allowing other actors to process their messages. When the I/O operation completes, a message is sent back to the original actor, which resumes processing. This message-passing and actor-centric approach is key to its performance.

Axum’s approach is more direct. When a request arrives, it’s handled by a chain of middleware and then passed to your service handler. The async/await system means that when an I/O operation is encountered within a handler, the await keyword suspends the current task, allowing the Tokio runtime to switch to another ready task. This cooperative multitasking means that the entire system is built around futures and their execution. The state management, as seen with AppState and Mutex, is a direct application of Tokio’s concurrency primitives.

The one thing most people don’t realize is how deeply Axum’s design is influenced by the desire for ergonomic composition with the broader Tokio ecosystem, not just raw speed. This means that if you’re already heavily invested in Tokio for background tasks, timers, or other asynchronous operations, Axum often feels like a natural extension. Its extractor system, for instance, is designed to be highly composable and extensible, allowing you to easily integrate custom logic or third-party libraries that also use Tokio’s FromRequest trait. This focus on integration can sometimes lead to code that is easier to reason about and maintain, even if it doesn’t always win in microbenchmarks.

The next step after choosing your framework is understanding how to manage application configuration, especially in production environments.

Want structured learning?

Take the full Rust course →