| |

Rust 85 🦀 High-Performance REST API Servers (axum and actix-web)

⏱️ Estimated read: 15 min | |

Rust’s two dominant web frameworks approach the same problem from opposite architectural directions. actix-web inherits the actor model from its actix runtime: each worker is a single-threaded Tokio runtime pinned to a thread, tasks stay on their worker, and communication happens through typed messages . axum is built directly on Tokio and the Tower ecosystem: handlers are plain async functions, extractors pull typed data from requests, and middleware composes through the generic Service and Layer traits . The performance difference between them is real but narrow, and the architectural difference determines which codebases fit which framework.

Benchmarks consistently show Actix Web with a throughput edge. In light-endpoint stress tests, Actix handled roughly 105,000 requests per second against Axum’s 79,000, a 33% lead . On heavier payloads the gap narrows to about 3% . A separate benchmark measured Actix at 74,886 requests per second against Axum at 47,174, though the authors cautioned the host was not idle . The direction is consistent: Actix is often faster, but the margin depends heavily on workload shape, and both frameworks exceed the throughput needs of typical CRUD services.

This chapter covers Actix Web’s actor-derived worker model and axum‘s Tower-based extraction and middleware architecture, the handler signatures each expects, the practical consequences of the middleware divide, and the decision criteria that matter more than raw benchmarks. It assumes familiarity with Tokio’s runtime model and Rust’s async traits from the preceding chapters.

Key point: Actix Web wins throughput benchmarks by pinning tasks to worker threads and eliminating work-stealing overhead. axum wins integration by aligning with Tokio, Hyper, and Tower. For most REST APIs, the Tower ecosystem and axum‘s extractor ergonomics matter more than a 3–33% throughput difference that neither framework’s users will notice at typical request volumes.


Why two frameworks with different architectures exist

The actor inheritance shapes Actix Web. actix-web is built on the actix actor library, which provides typed message passing between actors running in Arbiter execution contexts . An Arbiter controls one thread with one event loop. Actors spawned on an Arbiter stay there; they communicate with actors on other Arbiters through addresses and messages . This design predates Tokio’s dominance and carries a distinct programming model: state lives in actors, messages are statically typed, and handlers are methods on actor types.

The runtime model produces the performance difference. Actix Web spawns one single-threaded Tokio runtime per CPU core. Tasks are pinned to their worker thread, which eliminates work-stealing overhead and reduces cache-line contention under heavy load . Axum uses Tokio’s multi-threaded scheduler, where tasks can migrate between workers. The migration enables better load balancing but adds synchronization cost. The benchmark gap is the measurable consequence of this trade-off.

Tower is the integration story. axum depends on hyper for HTTP and tower for middleware . Tower’s Service trait is protocol-agnostic: a timeout layer, a tracing layer, or an authentication layer written for Tower works with axum, with tonic (gRPC), and with any other Tower-based service . Actix Web uses its own middleware system with Transform and Service traits that are not Tower-compatible . For teams already using Tower through Tonic or Hyper, this difference is architectural, not cosmetic.

The trade-off. Actix Web’s performance advantage is real but bounded. Its middleware ecosystem is smaller and incompatible with Tower. Its actor model, while powerful for certain stateful patterns, adds conceptual overhead for stateless REST handlers. Axum’s Tower alignment gives it a larger integration surface and a gentler learning curve, at the cost of a throughput edge that rarely materializes as a bottleneck outside extreme workloads .


a. Actix Web: workers, actors, and the #[actix_web::main] entry point

An Actix Web server starts through HttpServer, which spawns a number of worker threads. Each worker is a single-threaded Tokio runtime created by actix-rt . The server itself runs on the calling thread’s Tokio runtime, which accepts connections and distributes them to workers. Once a connection reaches a worker, its requests are handled entirely on that worker’s thread.

Handlers in Actix Web are async functions with extractors in their parameters. The extractors wrap the raw data in Actix-specific types: web::Path<T>, web::Query<T>, web::Json<T>. Each requires an .into_inner() call to access the underlying value . The framework’s actor heritage is visible in its state management: shared application state is passed as web::Data<T>, which wraps an Arc and is extracted in handler signatures.

use actix_web::{web, App, HttpServer, Responder};

// Application state wrapped for sharing across workers.
struct AppState {
    db_pool: String, // Placeholder for a real connection pool.
}

// Handler extracts path parameters and shared state.
// The web::Path wrapper requires into_inner() to access the value.
async fn get_user(
    path: web::Path<u32>,
    state: web::Data<AppState>,
) -> impl Responder {
    let user_id = path.into_inner();
    // Access shared state through the Data wrapper.
    format!("user {} from pool {}", user_id, state.db_pool)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // HttpServer spawns one worker per CPU core by default.
    // Each worker is a single-threaded Tokio runtime.
    HttpServer::new(|| {
        App::new()
            .app_data(web::Data::new(AppState {
                db_pool: "postgres://localhost".into(),
            }))
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

The #[actix_web::main] macro creates the runtime and runs the async main function. The HttpServer::new closure runs once per worker, constructing a separate App instance for each. This means application state must be cheaply cloneable or wrapped in Arc; web::Data provides the wrapping.

Actix Web’s middleware model uses the Transform and Service traits from its own ecosystem. A logging middleware implements these traits and is registered with .wrap(). The middleware runs on the worker thread, has access to the request and response, and can short-circuit or modify either. The model is capable but distinct from Tower: middleware written for Tower cannot be used without an adapter .


b. axum: Tower services, typed extractors, and IntoResponse

axum‘s handlers are async functions whose parameters implement FromRequestParts or FromRequest. The Path<T>, Query<T>, and Json<T> extractors deserialize and validate request data at the type level. State is extracted through State<T>, which is more type-safe than Actix’s Data<T> because it is checked at compile time rather than runtime . Handlers return anything that implements IntoResponse: &'static str, Json<T>, (StatusCode, T), or Result<T, E> where both T and E implement IntoResponse .

use axum::{
    extract::{Path, State},
    routing::get,
    Json, Router,
};
use std::sync::Arc;

struct AppState {
    db_pool: String,
}

// Handler extracts path and state through typed extractors.
// No into_inner() calls; Path<u32> dereferences directly.
async fn get_user(
    Path(user_id): Path<u32>,
    State(state): State<Arc<AppState>>,
) -> Json<String> {
    Json(format!("user {} from pool {}", user_id, state.db_pool))
}

#[tokio::main]
async fn main() {
    let state = Arc::new(AppState {
        db_pool: "postgres://localhost".into(),
    });

    // Router composes routes; with_state injects shared state.
    let app = Router::new()
        .route("/users/{id}", get(get_user))
        .with_state(state);

    // axum::serve is a thin wrapper over hyper's server.
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

The Router type composes routes and applies Tower layers. A timeout layer from tower-http wraps the entire router; an authentication layer wraps a subset of routes. The same layer can wrap a Tonic gRPC service, which is why axum is the default choice for mixed HTTP/gRPC services . Middleware is written once and reused across protocols.

axum‘s error model composes with Rust’s ? operator. A handler returning Result<Json<T>, AppError> propagates errors through ?, and AppError implements IntoResponse to convert the error into an HTTP response. This pattern is idiomatic Rust: errors are values, propagation is explicit, and the conversion to HTTP is a trait implementation rather than a framework callback.


Complete Example Session

// ============================================
// PART 1: AXUM — EXTRACTORS AND STATE
// ============================================
use axum::{
    extract::{Path, Query, State},
    routing::{get, post},
    Json, Router,
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    users: Arc<tokio::sync::RwLock<Vec<User>>>,
}

#[derive(Clone, Serialize, Deserialize)]
struct User {
    id: u32,
    name: String,
}

// Handler with Path and State extractors.
async fn get_user(
    Path(id): Path<u32>,
    State(state): State<AppState>,
) -> Result<Json<User>, StatusCode> {
    let users = state.users.read().await;
    users
        .iter()
        .find(|u| u.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// Handler with Json body extractor and IntoResponse return.
async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<User>,
) -> (StatusCode, Json<User>) {
    let mut users = state.users.write().await;
    users.push(payload.clone());
    (StatusCode::CREATED, Json(payload))
}

// ============================================
// PART 2: AXUM — ROUTER COMPOSITION
// ============================================
fn axum_app() -> Router {
    let state = AppState {
        users: Arc::new(tokio::sync::RwLock::new(vec![])),
    };

    Router::new()
        .route("/users/{id}", get(get_user))
        .route("/users", post(create_user))
        .with_state(state)
}

// ============================================
// PART 3: AXUM — TOWER MIDDLEWARE
// ============================================
use tower_http::trace::TraceLayer;
use std::time::Duration;

fn axum_with_middleware() -> Router {
    axum_app()
        // Tower layer: logs every request.
        .layer(TraceLayer::new_for_http())
        // Tower layer: 30-second timeout on all routes.
        .layer(tower_http::timeout::TimeoutLayer::new(
            Duration::from_secs(30),
        ))
}

// ============================================
// PART 4: ACTIX WEB — HANDLERS AND STATE
// ============================================
use actix_web::{web, App, HttpServer, Responder};

struct ActixState {
    users: tokio::sync::RwLock<Vec<User>>,
}

async fn actix_get_user(
    path: web::Path<u32>,
    state: web::Data<ActixState>,
) -> impl Responder {
    let id = path.into_inner();
    let users = state.users.read().await;
    match users.iter().find(|u| u.id == id) {
        Some(user) => web::Json(user.clone()),
        None => web::Json(User { id: 0, name: "not found".into() }),
    }
}

// ============================================
// PART 5: ACTIX WEB — SERVER STARTUP
// ============================================
// #[actix_web::main]
// async fn actix_main() -> std::io::Result<()> {
//     let state = web::Data::new(ActixState {
//         users: tokio::sync::RwLock::new(vec![]),
//     });
//
//     HttpServer::new(move || {
//         App::new()
//             .app_data(state.clone())
//             .route("/users/{id}", web::get().to(actix_get_user))
//     })
//     .bind("127.0.0.1:8080")?
//     .run()
//     .await
// }

// ============================================
// PART 6: THE MIDDLEWARE DIVIDE
// ============================================
// Axum: .layer(TraceLayer) — Tower ecosystem.
//   Works with tonic (gRPC), hyper services.
//   Reusable across protocols.
//
// Actix: .wrap(Logger::default()) — Actix middleware.
//   Actix-specific Transform/Service traits.
//   Not interchangeable with Tower.

// ============================================
// PART 7: EXTRACTOR ERGONOMICS
// ============================================
// Axum: Path(id): Path<u32> — destructured directly.
// Actix: web::Path<u32> — requires .into_inner().

// ============================================
// PART 8: ERROR HANDLING
// ============================================
// Axum: Result<T, E> where E: IntoResponse.
//   Composes with ? operator.
//
// Actix: ResponseError trait.
//   Requires impl Display + ResponseError.

// ============================================
// PART 9: PERFORMANCE CONTEXT
// ============================================
// Actix: single-threaded runtime per worker,
//   pinned tasks, no work-stealing overhead.
//   Benchmark edge: 3–33% depending on workload.
//
// Axum: Tokio multi-threaded, work-stealing.
//   Better load balancing, slightly more overhead.

// ============================================
// PART 10: DECISION CRITERIA
// ============================================
// Choose Axum:
//   - New project, Tokio ecosystem.
//   - Mixed HTTP + gRPC (Tower reuse).
//   - Team new to Rust (extractor ergonomics).
//   - Microservices needing Tower middleware.
//
// Choose Actix Web:
//   - Maximum throughput (ad tech, analytics).
//   - WebSocket-heavy, battle-tested.
//   - Existing Actix codebase.
//   - Team familiarity with actor model.

This session demonstrates both frameworks side by side: axum‘s typed extractors and Tower layers, Actix’s web::Data state and into_inner() extraction, and the middleware divide that determines which third-party layers you can reuse. The decision criteria are the practical summary: ecosystem integration for axum, raw throughput and WebSocket maturity for Actix.


Quick Reference

Architecture Comparison

Aspectaxumactix-web
RuntimeTokio multi-threadedSingle-threaded Tokio per worker
Task schedulingWork-stealingPinned to worker thread
MiddlewareTower Service/LayerActix Transform/Service
State extractionState<T> (compile-time safe)Data<T> (runtime wrapper)
Extractor styleDestructured parametersweb::Path<T> + .into_inner()
Error modelIntoResponse + ?ResponseError trait

Benchmark Snapshot

WorkloadActixAxumGap
Light endpoint (req/s)~105,000~79,000Actix +33%
Heavy endpoint (req/s)~220~214Actix +3%
RAM-intensive (req/s)~96~101Axum +5%
Mixed benchmark (req/s)~74,900~47,200Actix +59%

Ecosystem Integration

Needaxumactix-web
gRPC (Tonic)Tower middleware sharedAdapter required
Tower middlewareNativeIncompatible
WebSocketaxum::extract::wsBattle-tested
Database (SQLx)runtime-tokioruntime-actix or Tokio

When to Choose

ScenarioRecommendation
New Rust project, 2026axum
Mixed HTTP + gRPCaxum (Tower)
Max throughput (ad tech)Actix Web
WebSocket-heavyActix Web
Team new to Rustaxum
Existing Actix codebaseActix Web

Best Practices

✅ Do This:

// Axum: use typed State extractors.                        // ✅
async fn h(State(s): State<AppState>) -> Json<T>

// Axum: compose middleware with Tower layers.              // ✅
.layer(TraceLayer::new_for_http())

// Actix: wrap shared state in web::Data.                   // ✅
.app_data(web::Data::new(state))

// Actix: use .into_inner() for path parameters.            // ✅
let id = path.into_inner();

// Both: use SQLx for compile-time checked queries.         // ✅
sqlx::query_as!(User, "SELECT ...")

❌ Don’t Do This:

// Axum: extract State with Extension (runtime error).      // ❌
Extension<AppState> // 500 if layer missing.

// Actix: share state without Arc/Data wrapping.            // ❌
// State must be cloneable across workers.

// Axum: assume Tower middleware works in Actix.            // ❌
// Different trait systems entirely.

// Actix: use Tower layers directly.                        // ❌
// Requires adapter or rewrite.

Common Pitfalls

PitfallWhy It HappensFix
Axum Extension 500 errorLayer not applied or wrong typeUse State<T> or verify layer
Actix state not sharedweb::Data not cloned per worker.app_data(data.clone())
Tower middleware fails in ActixIncompatible trait systemsUse Actix-native middleware
into_inner() forgottenActix path extractor wrapperCall .into_inner()
Benchmark-driven choiceThroughput gap rarely mattersEvaluate ecosystem fit first

Real-World Examples

1. Axum Typed Path

async fn user(Path(id): Path<u32>) -> Json<User> { ... }

2. Actix Path with into_inner

async fn user(path: web::Path<u32>) -> impl Responder { path.into_inner(); }

3. Axum Tower Timeout

.layer(tower_http::timeout::TimeoutLayer::new(Duration::from_secs(30)))

4. Actix Logger Middleware

.wrap(actix_web::middleware::Logger::default())

5. Axum State Sharing

Router::new().with_state(Arc::new(AppState { ... }))

6. Actix Data Sharing

.app_data(web::Data::new(AppState { ... }))

7. Axum Error Propagation

async fn h() -> Result<Json<T>, AppError> { let x = db.fetch().await?; Ok(Json(x)) }

8. Actix ResponseError

impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { ... } }

9. Axum gRPC Integration

// Same TraceLayer wraps tonic service and axum Router.

10. Actix WebSocket Actor

// WsChatSession actor implements StreamHandler for WebSocket messages.

Visual

Actix Worker Model

┌──────────────────────────────────────────────┐
│  ACTIX WEB SERVER                            │
│                                              │
│  HttpServer (accepts connections)            │
│       │                                      │
│       ├──► Worker 0 (thread 0)               │
│       │      Single-threaded Tokio runtime   │
│       │      Tasks pinned here               │
│       │                                      │
│       ├──► Worker 1 (thread 1)               │
│       │      Single-threaded Tokio runtime   │
│       │      Tasks pinned here               │
│       │                                      │
│       └──► Worker N                          │
│                                              │
│  No work-stealing between workers.           │
│  Actors communicate via messages.            │
│  Performance: cache locality, no migration.  │
│                                              │
└──────────────────────────────────────────────┘

Axum Tower Architecture

┌──────────────────────────────────────────────┐
│  AXUM STACK                                  │
│                                              │
│  ┌────────────────────────────────────┐      │
│  │  Tower Layer (TraceLayer)          │      │
│  │  Protocol-agnostic Service         │      │
│  └────────────────────────────────────┘      │
│         │                                    │
│         ▼                                    │
│  ┌────────────────────────────────────┐      │
│  │  Axum Router                       │      │
│  │  Handler: async fn(Path, State)    │      │
│  └────────────────────────────────────┘      │
│         │                                    │
│         ▼                                    │
│  ┌────────────────────────────────────┐      │
│  │  Hyper (HTTP/1.1, HTTP/2)          │      │
│  └────────────────────────────────────┘      │
│                                              │
│  Same Layer wraps Tonic gRPC service.        │
│  Middleware written once, reused.            │
│                                              │
└──────────────────────────────────────────────┘

Middleware Divide

┌──────────────────────────────────────────────┐
│  TOWER ECOSYSTEM         ACTIX ECOSYSTEM     │
│                                              │
│  Service<Request>        Transform           │
│  Layer                   Service             │
│                                              │
│  Works with:             Works with:         │
│    axum                    actix-web         │
│    tonic (gRPC)                              │
│    hyper                                     │
│                                              │
│  NOT compatible:                             │
│    Tower ↔ Actix                             │
│                                              │
│  Choose based on what you need to reuse.     │
│                                              │
└──────────────────────────────────────────────┘

Decision Flow

┌──────────────────────────────────────────────┐
│  WHICH FRAMEWORK?                            │
│                                              │
│  New project + Tokio ecosystem?              │
│       └── Axum                               │
│                                              │
│  Mixed HTTP + gRPC?                          │
│       └── Axum (Tower reuse)                 │
│                                              │
│  Max throughput, ad tech, analytics?         │
│       └── Actix Web                          │
│                                              │
│  WebSocket-heavy, production-proven?         │
│       └── Actix Web                          │
│                                              │
│  Existing Actix codebase?                    │
│       └── Stay with Actix                    │
│                                              │
│  Default for most new REST APIs: Axum.       │
│                                              │
└──────────────────────────────────────────────┘

Summary

Itemaxumactix-web
RuntimeTokio multi-threadedSingle-threaded per worker
Task migrationWork-stealingPinned
MiddlewareTower LayerActix Transform
StateState<T>web::Data<T>
ExtractorDestructured.into_inner()
ErrorIntoResponse + ?ResponseError
gRPC sharingTower nativeAdapter needed
ThroughputCompetitiveOften faster
Learning curveGentlerSteeper
Default choice (2026)New projectsPerformance-critical

Key takeaways:

  • Actix Web pins tasks to worker threads; axum lets Tokio’s scheduler migrate them. The pinned model reduces cache contention and produces benchmark wins of 3–33% depending on workload .
  • The middleware divide is the architectural fork. Tower layers work with axum, tonic, and hyper. Actix middleware is incompatible with all three. For mixed HTTP/gRPC services, this decides the framework .
  • Extractor ergonomics favor axum. Path(id): Path<u32> destructures directly; Actix’s web::Path<u32> requires .into_inner(). The difference is small per handler but accumulates across a codebase .
  • State sharing differs in type safety. axum‘s State<T> is checked at compile time; Actix’s web::Data<T> wraps an Arc and relies on extraction at runtime .
  • Benchmarks require context. The Actix throughput edge is real but rarely the bottleneck for CRUD APIs. A benchmark showing Actix at 105k req/s and Axum at 79k req/s describes a workload far beyond typical service traffic.
  • Ecosystem trajectory favors axum. It is maintained by the Tokio team, aligns with Hyper and Tower, and is the default choice for new projects in architectural decision records . Actix remains the correct choice for existing codebases and throughput-critical services.
  • WebSocket maturity belongs to Actix. Its actor-based WebSocket handling has a longer production track record; axum‘s support works but is newer .

Remember: The choice between axum and actix-web is less about throughput than about architecture and ecosystem. Actix Web’s pinned worker model and actor heritage produce measurable performance advantages and a mature WebSocket story. Axum’s Tower integration produces a reusable middleware layer that spans HTTP and gRPC, typed extractors that compose with Rust’s error handling, and alignment with the Tokio ecosystem that most new projects already depend on. The benchmark difference matters at the margins; the middleware and ecosystem differences matter on every request handler you write. For most new REST APIs, axum is the default; for throughput-critical or WebSocket-heavy services, Actix Web earns its keep.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!