MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Rust Web Frameworks: Actix-Web, Axum, and Rocket Compared

Compare Rust web frameworks: performance, ergonomics, ecosystem, and when to use each.

RustActixAxumBackend

By MinhVo

Introduction

The Rust ecosystem has matured dramatically over the past several years, and web development is no exception. What started as a handful of experimental HTTP libraries has grown into a rich landscape of production-ready frameworks that rival—and often surpass—the performance of Go, Node.js, and even C++ web servers. But choosing the right Rust web framework requires understanding their philosophical differences, not just their benchmark numbers.

Actix-Web burst onto the scene as one of the first truly high-performance Rust frameworks, consistently topping TechEmpower benchmarks. Axum, built by the Tokio team, emerged later with a focus on ergonomics and composability using Tower middleware. Rocket took a different path entirely, prioritizing developer experience with its macro-driven API that feels almost magical. Each represents a distinct philosophy about what Rust web development should look like.

In this guide, we will dig deep into all three frameworks. You will learn their architectural underpinnings, see real production code patterns, understand when each one shines, and walk away with a clear decision framework for your next project.

Rust web frameworks ecosystem

Understanding Rust Web Frameworks: Core Concepts

Before comparing these frameworks, it helps to understand what they share. All three are built on top of the asynchronous Rust runtime—primarily Tokio—and they all leverage Rust's ownership model to provide memory safety without garbage collection. This means you get predictable latency, low memory footprint, and fearless concurrency as baseline guarantees.

The fundamental abstraction in any Rust web framework is the request-response cycle. A client sends an HTTP request, the framework parses it into a structured Request object, routes it to a handler function, and the handler produces a Response. Where the frameworks diverge is in how they express this cycle: how you define routes, how you extract data from requests, how you compose middleware, and how you return responses.

Actix-Web uses an actor-based model under the hood (borrowed from Erlang's OTP), where each connection can be managed by a lightweight actor. This was revolutionary when it launched but has since shifted to a more conventional async/await model. Axum embraces Tower's Service trait, treating every middleware layer as a composable, reusable service. Rocket uses procedural macros extensively, letting you declare your intent through attributes and letting the compiler figure out the plumbing.

Understanding these philosophical roots is critical because they affect everything from how you write tests to how you handle errors to how your team will onboard new developers.

Architecture and Design Patterns

Actix-Web: Actor-Inspired Request Handling

Actix-Web's architecture is built around the concept of extractors and responders. When a request arrives, the framework matches it against registered routes and then runs a series of extractors to pull data from the request—path parameters, query strings, JSON bodies, headers, and more. Your handler function receives these extracted values as arguments and returns something that implements the Responder trait.

use actix_web::{web, App, HttpServer, HttpResponse};
 
async fn get_user(path: web::Path<u32>) -> HttpResponse {
    let user_id = path.into_inner();
    HttpResponse::Ok().json(serde_json::json!({"id": user_id}))
}

The middleware system in Actix-Web wraps around handlers using a Transform and Service pattern. You can apply middleware globally, per-scope, or per-resource. Logging, authentication, CORS, and rate limiting are all implemented as middleware that wraps the inner service.

Axum: Tower-Powered Composability

Axum's defining architectural choice is its deep integration with the Tower ecosystem. Every handler in Axum is a Tower Service, and every middleware is a Tower Layer. This means you can use any Tower-compatible middleware (like tower-http's CORS, compression, or tracing layers) without any adapter code.

use axum::{routing::get, Router, extract::Path, Json};
use tower_http::cors::CorsLayer;
 
async fn get_user(Path(user_id): Path<u32>) -> Json<serde_json::Value> {
    Json(serde_json::json!({"id": user_id}))
}
 
let app = Router::new()
    .route("/users/{id}", get(get_user))
    .layer(CorsLayer::permissive());

Axum's extractor system uses Rust's FromRequest trait, which you can implement for custom types. The framework validates and extracts data before your handler runs, meaning your handler only executes when all inputs are valid.

Rocket: Macro-Driven Simplicity

Rocket takes the most opinionated approach. You annotate your functions with #[get], #[post], or other route macros, and Rocket's code generation handles the rest. Request guards, form handling, JSON serialization, and even database connections are expressed through types and macros.

#[macro_use] extern crate rocket;
 
#[get("/users/<user_id>")]
fn get_user(user_id: u32) -> Json<serde_json::Value> {
    Json(serde_json::json!({"id": user_id}))
}
 
#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![get_user])
}

Rocket's request guards are particularly powerful. By implementing the FromRequest trait, you can create custom guards that validate authentication tokens, check database connections, or enforce rate limits—all expressed as function parameters.

Framework architecture comparison

Step-by-Step Implementation

Setting Up an Actix-Web Project

Let's build a complete REST API with Actix-Web:

[dependencies]
actix-web = "4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Now let's implement a full CRUD API with error handling and middleware:

use actix_web::{web, App, HttpServer, HttpResponse, middleware};
use serde::{Deserialize, Serialize};
 
#[derive(Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String,
}
 
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}
 
async fn create_user(body: web::Json<CreateUser>) -> HttpResponse {
    let user = User {
        id: 1,
        name: body.name.clone(),
        email: body.email.clone(),
    };
    HttpResponse::Created().json(user)
}
 
async fn get_users() -> HttpResponse {
    let users: Vec<User> = vec![];
    HttpResponse::Ok().json(users)
}
 
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(middleware::Logger::default())
            .service(
                web::scope("/api")
                    .route("/users", web::get().to(get_users))
                    .route("/users", web::post().to(create_user))
            )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Building the Same API with Axum

Axum's equivalent implementation highlights the different ergonomic approach:

[dependencies]
axum = "0.7"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
use axum::{
    routing::{get, post},
    Router, Json, extract::State,
    http::StatusCode,
};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
 
#[derive(Clone)]
struct AppState {
    users: Arc<Mutex<Vec<User>>>,
}
 
#[derive(Serialize, Deserialize, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}
 
#[derive(Deserialize)]
struct CreateUser {
    name: String,
    email: String,
}
 
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    let mut users = state.users.lock().unwrap();
    let user = User {
        id: users.len() as u32 + 1,
        name: payload.name,
        email: payload.email,
    };
    users.push(user.clone());
    (StatusCode::CREATED, Json(user))
}
 
async fn get_users(State(state): State<AppState>) -> Json<Vec<User>> {
    let users = state.users.lock().unwrap();
    Json(users.clone())
}
 
#[tokio::main]
async fn main() {
    let state = AppState {
        users: Arc::new(Mutex::new(Vec::new())),
    };
 
    let app = Router::new()
        .route("/api/users", get(get_users).post(create_user))
        .with_state(state)
        .layer(tower_http::trace::TraceLayer::new_for_http());
 
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

Rocket Implementation

#[macro_use] extern crate rocket;
 
use rocket::serde::{json::Json, Deserialize, Serialize};
 
#[derive(Serialize, Deserialize, Clone)]
#[serde(crate = "rocket::serde")]
struct User {
    id: u32,
    name: String,
    email: String,
}
 
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct CreateUser {
    name: String,
    email: String,
}
 
#[get("/api/users")]
fn get_users() -> Json<Vec<User>> {
    Json(vec![])
}
 
#[post("/api/users", data = "<body>")]
fn create_user(body: Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        name: body.name.clone(),
        email: body.email.clone(),
    })
}
 
#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![get_users, create_user])
}

Real-World Use Cases and Case Studies

Use Case 1: High-Throughput API Gateway

When Cloudflare needed to handle millions of requests per second with minimal latency, they evaluated Rust frameworks. Actix-Web's raw performance made it the natural choice for their proxy layer. Its ability to handle tens of thousands of concurrent connections with minimal memory overhead—typically under 1MB per 10,000 idle connections—proved decisive. The framework's mature HTTP/1.1 and HTTP/2 implementations, combined with its battle-tested TLS support, provided the reliability Cloudflare required.

Use Case 2: Microservice Architecture

For teams building microservices on Tokio, Axum is increasingly the default choice. Its Tower integration means you can share middleware between your gRPC services (using tonic) and your HTTP services. A fintech startup reported reducing their middleware boilerplate by 60% after switching from Actix-Web to Axum because they could reuse their authentication, logging, and tracing layers across all services.

Use Case 3: Rapid Prototyping and Internal Tools

Rocket excels when developer velocity matters more than raw throughput. A data science team at a mid-sized company built their entire internal dashboard backend in Rocket in a single sprint. The macro-driven API meant junior Rust developers could write handlers without understanding extractors, request guards, or the type system's advanced features. They reported 3x faster development compared to their previous Go backend.

Use Case 4: WebSocket and Real-Time Applications

Actix-Web has the most mature WebSocket support of the three, with built-in actors for managing long-lived connections. A chat application serving 50,000 concurrent WebSocket connections reported stable memory usage at around 200MB with Actix-Web, compared to 1.2GB with their previous Node.js implementation. Axum has caught up significantly with its WebSocket support via axum::extract::ws, but Actix-Web's actor model provides more natural patterns for connection lifecycle management.

Best Practices for Production

  1. Choose extractors wisely: All three frameworks support custom extractors, but Axum's FromRequest trait is the most composable. Build domain-specific extractors for authentication, pagination, and request validation to keep handlers clean.

  2. Use structured logging from day one: Actix-Web integrates with env_logger, Axum with tracing, and Rocket with its own logging system. Set up structured JSON logging before you deploy, not after your first production incident.

  3. Handle errors at the framework boundary: Define a unified error type that converts to HTTP responses. In Axum, implement IntoResponse for your error type. In Actix-Web, implement ResponseError. In Rocket, use the Responder trait.

  4. Leverage connection pooling: Use sqlx or deadpool for database connections. All three frameworks support shared state—use it to maintain connection pools rather than creating connections per request.

  5. Test with integration tests: Use actix_web::test, axum::test, or Rocket's built-in testing utilities to write HTTP-level integration tests rather than unit-testing handlers in isolation.

  6. Profile before optimizing: Use cargo flamegraph to identify bottlenecks. The framework itself is rarely the bottleneck—database queries, serialization, and business logic dominate latency profiles.

  7. Use graceful shutdown: All three frameworks support graceful shutdown. Implement it so that in-flight requests complete during deployments rather than being abruptly terminated.

  8. Pin dependency versions: Rust's ecosystem moves fast. Pin your framework version in Cargo.toml and use cargo update deliberately. Breaking changes in minor versions are rare but not impossible.

Common Pitfalls and Solutions

PitfallImpactSolution
Blocking the async runtime with synchronous codeAll requests stall, latency spikesUse tokio::task::spawn_blocking for CPU-bound or blocking I/O work
Excessive cloning in handlersUnnecessary memory allocationsUse references in extractors where possible; implement Borrow for shared state
Ignoring connection pool exhaustionService becomes unresponsive under loadSet pool size limits, implement timeouts, monitor pool metrics
Rocket macro magic hiding complexityHard-to-debug compilation errorsUnderstand what the macros expand to using cargo expand
Actix-Web actor system confusionResource leaks if actors aren't properly stoppedUse actix_web::rt::spawn for background tasks, not raw actors
Missing error propagationSilent failures in productionImplement From<DbError> for your app error type to force explicit handling

Performance Optimization

All three frameworks can serve hundreds of thousands of requests per second for simple JSON endpoints. The real performance differences emerge in middleware-heavy workloads and complex extraction logic.

// Axum: Optimize JSON extraction with custom limits
use axum::extract::DefaultBodyLimit;
 
let app = Router::new()
    .route("/api/upload", post(upload_handler))
    .layer(DefaultBodyLimit::max(10 * 1024 * 1024)); // 10MB limit
// Actix-Web: Connection-level optimizations
use actix_web::HttpServer;
 
HttpServer::new(|| App::new())
    .keep_alive(Duration::from_secs(75))
    .client_request_timeout(Duration::from_secs(5))
    .client_disconnect_timeout(Duration::from_secs(5))
    .max_connections(10000)
    .bind("0.0.0.0:8080")?
    .run()
    .await

For database-bound applications, the bottleneck is almost always the database, not the framework. Use EXPLAIN ANALYZE on your queries, implement proper indexing, and consider read replicas before blaming the web framework for latency.

Comparison with Alternatives

FeatureActix-WebAxumRocket
PerformanceExcellent (top TechEmpower)Excellent (slightly behind Actix)Good (slightly slower due to macro overhead)
Learning CurveModerateModerateLow (macro magic helps beginners)
MiddlewareCustom Transform/ServiceTower ecosystemFairings
WebSocketNative actor supportaxum::extract::wsVia rocket_ws contrib
Community SizeLargestGrowing rapidlyStable
Async RuntimeTokio (default)Tokio (required)Tokio (0.5+)
TLS Supportrustls / native-tlsVia axum-serverBuilt-in
HTTP/2SupportedSupportedSupported
MaturityMost matureRapidly maturingMature
MaintenanceActiveVery active (Tokio team)Active

Advanced Patterns and Techniques

Custom Extractors with Validation

// Axum: Reusable pagination extractor
use axum::{
    extract::{FromRequestParts, Query},
    http::request::Parts,
};
 
struct Pagination {
    page: u32,
    per_page: u32,
}
 
#[derive(Deserialize)]
struct PaginationParams {
    page: Option<u32>,
    per_page: Option<u32>,
}
 
#[axum::async_trait]
impl<S> FromRequestParts<S> for Pagination
where
    S: Send + Sync,
{
    type Rejection = (axum::http::StatusCode, String);
 
    async fn from_request_parts(
        parts: &mut Parts,
        state: &S,
    ) -> Result<Self, Self::Rejection> {
        let Query(params) = Query::<PaginationParams>::from_request_parts(parts, state)
            .await
            .map_err(|_| (StatusCode::BAD_REQUEST, "Invalid query".to_string()))?;
 
        Ok(Pagination {
            page: params.page.unwrap_or(1).max(1),
            per_page: params.per_page.unwrap_or(20).min(100),
        })
    }
}

Tower Middleware for Rate Limiting

use tower::ServiceBuilder;
use tower_http::limit::RequestBodyLimitLayer;
 
let app = Router::new()
    .route("/api/data", get(handler))
    .layer(
        ServiceBuilder::new()
            .layer(RequestBodyLimitLayer::new(1024 * 1024))
            .layer(tower_http::trace::TraceLayer::new_for_http())
            .layer(tower_http::compression::CompressionLayer::new())
    );

Testing Strategies

// Axum integration test
#[tokio::test]
async fn test_create_user() {
    let app = Router::new()
        .route("/api/users", post(create_user))
        .with_state(test_state());
 
    let response = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/api/users")
                .header("content-type", "application/json")
                .body(Body::from(r#"{"name":"Test","email":"t@t.com"}"#))
                .unwrap(),
        )
        .await
        .unwrap();
 
    assert_eq!(response.status(), StatusCode::CREATED);
}
 
// Actix-Web integration test
#[actix_web::test]
async fn test_create_user_actix() {
    let app = actix_web::test::init_service(
        App::new().route("/api/users", web::post().to(create_user))
    ).await;
 
    let req = actix_web::test::TestRequest::post()
        .uri("/api/users")
        .set_json(serde_json::json!({"name": "Test", "email": "t@t.com"}))
        .to_request();
 
    let resp = actix_web::test::call_service(&app, req).await;
    assert_eq!(resp.status(), 201);
}

Future Outlook

The Rust web framework landscape is converging around shared infrastructure. Axum's Tower integration is becoming the de facto standard for middleware composition, and even Actix-Web has moved toward a more service-oriented architecture. We are seeing the emergence of framework-agnostic crates for authentication, database access (sqlx, sea-orm), and OpenAPI generation (utoipa, aide).

The introduction of Rust's async trait stabilization in 1.75 has simplified custom extractor and middleware development across all frameworks. Looking ahead, HTTP/3 support via quinn and h3 is on the roadmap for all three frameworks, which will further reduce the performance gap with Go and eliminate it with Node.js.

Conclusion

Choosing between Actix-Web, Axum, and Rocket comes down to your priorities. If raw performance and maximum control matter most, Actix-Web remains the king of benchmarks with the largest ecosystem and most battle-tested production deployments. If you value composability and are already in the Tokio ecosystem, Axum's Tower integration provides unmatched middleware reuse and the most modern API design. If developer experience and rapid prototyping are paramount, Rocket's macro-driven approach gets you to a working API fastest.

Key takeaways:

  1. All three frameworks deliver production-grade performance far beyond most use cases
  2. Axum's Tower integration makes it the most composable option for microservices
  3. Actix-Web's actor model provides unique patterns for WebSocket-heavy applications
  4. Rocket's macros dramatically reduce boilerplate for CRUD-heavy backends
  5. The real performance bottleneck is almost always your database, not your framework

Start with Axum for new projects unless you have a specific reason to choose otherwise. Migrate to Actix-Web if you hit performance walls (unlikely) or need its actor model. Use Rocket for internal tools and prototypes where development speed is the priority.