Oct 23, 20268 min read

Axum vs Actix Web vs Rocket

How the three main Rust web frameworks compare on routing, extractors, middleware, state, runtime model and ecosystem, with the same endpoint written in each.

Axum, Actix Web and Rocket are the three Rust web frameworks most teams shortlist, and all three run async handlers on Tokio with Serde for JSON. They differ in how routes are declared, how state and middleware are passed around and which ecosystem of crates they plug into, and those differences show up once a service grows past a few handlers and needs shared database pools, auth middleware and tracing that work the same way across every route.

The main differences:

  • Axum uses plain async functions with extractors and Tower middleware, while Actix Web and Rocket declare routes with attribute macros.
  • Actix Web runs each worker on its own single-threaded runtime, while Axum and Rocket use a multi-threaded Tokio runtime by default.
  • Axum shares middleware with the Tower and Hyper ecosystem, Actix Web has its own middleware trait and Rocket uses fairings.
  • None of the three provision a database, broker or bucket.

Axum vs Actix Web vs Rocket at a glance

OptionApproachBest fit
EncoreInfrastructure SDK you add to whichever framework you pick, such as AxumTeams that want Postgres, Pub/Sub and buckets handled locally and in their own cloud
AxumRouter with async functions, extractors and Tower layersMost new Rust APIs
Actix WebMature framework with attribute macros and first-party cratesTeams that want many built-in features and a long track record
RocketMacro-driven routes with request guards and fairingsTeams that value concise, type-checked route declarations
Compare features across all four options
CapabilityEncoreAxumActix WebRocket
RoleInfrastructure SDK you add to your frameworkWeb frameworkWeb frameworkWeb framework
Route declarationFrom AxumRouter::new().route(...)#[post(...)] macros or App::route#[post(...)] macros mounted with routes![]
Request parsingFrom AxumExtractors (Json, Path, State)Extractors (web::Json, web::Path, web::Data)Request guards and data = "<...>"
Shared stateInfrastructure is a static, no state to passState via with_stateweb::Data via app_dataState via manage
MiddlewareFrom AxumTower layersActix Transform middlewareFairings
Runtime modelFrom AxumMulti-threaded TokioSingle-threaded Tokio runtime per workerMulti-threaded Tokio
Database and Pub/SubDeclared in code and provisionedBring your ownBring your ownrocket_db_pools for databases
Production infrastructureProvisioned in your AWS or GCP accountYou provision itYou provision itYou provision it

Encore: an infrastructure SDK for the framework you choose

Encore

Encore combines the Axum service you already have with infrastructure declared as static values in the same crate, as an SDK you add rather than a framework you switch to, so the router, extractors and Tower layers stay as they are while Postgres databases, Pub/Sub topics, object storage buckets, Redis and cron jobs become declarations that Encore provisions. encore run starts the database, topics and buckets locally and applies migrations, cron jobs run only where the app is deployed, and a deploy provisions them in your own AWS or GCP account, which removes the State plumbing that the frameworks below use for pools and clients.

Application code passes through Encore and infrastructure guardrails to resources provisioned in your own AWS or GCP account.
Encore provisions the infrastructure your application needs in your own AWS or GCP account.

The signup endpoint that the framework sections below write to Postgres looks like this with Encore and Axum, where it also publishes an event:

use axum::{http::StatusCode, routing::post, Json, Router}; use encore::pubsub::Topic; use encore::storage::postgres::Database; use serde::{Deserialize, Serialize}; static DB: Database = Database::new("users").migrations("./migrations"); static SIGNUPS: Topic<Signup> = Topic::new("signups"); #[derive(Serialize, Deserialize)] struct Signup { email: String, } async fn create_user(Json(req): Json<Signup>) -> Result<StatusCode, StatusCode> { DB.execute("INSERT INTO users (email) VALUES ($1)", &[&req.email]) .await .map_err(internal)?; SIGNUPS.publish(&req).await.map_err(internal)?; Ok(StatusCode::CREATED) } fn internal(_: encore::Error) -> StatusCode { StatusCode::INTERNAL_SERVER_ERROR } #[tokio::main] async fn main() { encore::initialize(); let app = Router::new().route("/users", post(create_user)); let addr = std::env::var("ENCORE_LISTEN_ADDR").unwrap_or("0.0.0.0:8080".into()); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app) .with_graceful_shutdown(encore::shutdown::signal()) .await .unwrap(); }

Reads use query_one and query_opt on the same declaration, and the row values come out with get:

async fn user_name(id: i64) -> Result<String, encore::Error> { let row = DB .query_one("SELECT name FROM users WHERE id = $1", &[&id]) .await?; Ok(row.get("name")) }

Key features

  • Postgres, Pub/Sub, object storage, Redis, cron jobs and secrets are static declarations that handlers use directly.
  • encore run starts the infrastructure locally, and encore test gives each test its own migrated database.
  • encore::shutdown::signal() plugs into Axum's graceful shutdown so in-flight work drains before exit.
  • A deploy provisions the same declarations in your own AWS or GCP account.
  • An AI coding agent giving a handler a database writes the Database::new static in the same change and runs it under encore run, with no Terraform to touch.

Good to know

Encore works with whichever of the three frameworks you pick, and Axum is the example used here. Rust support is in beta, and you can sign up for access.

Try Encore →

Axum: functions, extractors and Tower

Axum

Axum comes from the Tokio project, and a handler is an async function whose arguments are extractors and whose return value implements IntoResponse.

use axum::{extract::State, http::StatusCode, routing::post, Json, Router}; use serde::Deserialize; use sqlx::PgPool; #[derive(Deserialize)] struct Signup { email: String, } async fn create_user( State(pool): State<PgPool>, Json(req): Json<Signup>, ) -> Result<StatusCode, StatusCode> { sqlx::query("INSERT INTO users (email) VALUES ($1)") .bind(&req.email) .execute(&pool) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(StatusCode::CREATED) } #[tokio::main] async fn main() -> anyhow::Result<()> { let pool = PgPool::connect(&std::env::var("DATABASE_URL")?).await?; let app = Router::new() .route("/users", post(create_user)) .with_state(pool); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, app).await?; Ok(()) }
The pool from SQLx reaches the handler through State.

Key features

  • Routing needs no macros, and extractors are ordinary types you can write yourself.
  • Any Tower layer works as middleware, including tower-http for tracing, CORS, compression and timeouts.
  • State is checked at compile time, so a handler cannot ask for state the router does not provide.

Good to know

Compiler errors for handlers with a wrong extractor order can be hard to read, which #[debug_handler] from axum-macros improves. Axum 0.8 changed path parameters from /:id to /{id}.

Try Axum →

Actix Web: mature and feature-rich

Actix Web

Actix Web declares routes with attribute macros, passes shared state as web::Data and starts a number of workers that each run a single-threaded runtime.

use actix_web::{post, web, App, HttpResponse, HttpServer, Responder}; use serde::Deserialize; use sqlx::PgPool; #[derive(Deserialize)] struct Signup { email: String, } #[post("/users")] async fn create_user(pool: web::Data<PgPool>, req: web::Json<Signup>) -> impl Responder { match sqlx::query("INSERT INTO users (email) VALUES ($1)") .bind(&req.email) .execute(pool.get_ref()) .await { Ok(_) => HttpResponse::Created().finish(), Err(_) => HttpResponse::InternalServerError().finish(), } } #[actix_web::main] async fn main() -> std::io::Result<()> { let url = std::env::var("DATABASE_URL").expect("DATABASE_URL"); let pool = PgPool::connect(&url).await.expect("connect"); HttpServer::new(move || { App::new() .app_data(web::Data::new(pool.clone())) .service(create_user) }) .bind(("0.0.0.0", 8080))? .run() .await }

Key features

  • First-party crates cover WebSockets, sessions, multipart, CORS and static files.
  • The per-worker runtime model avoids cross-thread synchronization for worker-local state.
  • It has a long production history and strong benchmark results.

Good to know

The App factory closure runs once per worker, so shared state has to be cloned into it or wrapped in web::Data, and Actix middleware does not interoperate with Tower layers, which means fewer shared crates with the Axum ecosystem.

Try Actix Web →

Rocket: request guards and macros

Rocket

Rocket puts the method, path and body format in the route attribute, validates input through request guards and has its own configuration and database pool integration through rocket_db_pools, which the signup below uses to write to Postgres.

#[macro_use] extern crate rocket; use rocket::http::Status; use rocket::serde::{json::Json, Deserialize}; use rocket_db_pools::{sqlx, Connection, Database}; // The URL comes from Rocket.toml under [default.databases.users]. #[derive(Database)] #[database("users")] struct Users(sqlx::PgPool); #[derive(Deserialize)] #[serde(crate = "rocket::serde")] struct Signup { email: String, } #[post("/users", data = "<req>")] async fn create_user(mut db: Connection<Users>, req: Json<Signup>) -> Result<Status, Status> { sqlx::query("INSERT INTO users (email) VALUES ($1)") .bind(&req.email) .execute(&mut **db) .await .map_err(|_| Status::InternalServerError)?; Ok(Status::Created) } #[launch] fn rocket() -> _ { rocket::build().attach(Users::init()).mount("/", routes![create_user]) }

Key features

  • Request guards turn authentication and validation into types on the handler signature.
  • Rocket.toml handles configuration per profile.
  • rocket_db_pools manages database pools, and fairings hook into the request lifecycle.

Good to know

Rocket ships releases less often than Axum and Actix Web, and its macro-generated code can make custom patterns harder to express, so check current release activity before committing a large codebase to it.

Try Rocket →

Performance

All three frameworks serve far more requests per second than most APIs receive. Database queries, serialization of large payloads and calls to other services usually decide the real response time, so ergonomics and ecosystem fit tend to matter more than raw throughput when choosing.

How to choose

  • Encore added to whichever framework you pick, such as Axum, when you want Postgres, Pub/Sub, buckets and cron jobs declared in Rust, with the database, topics and buckets running locally and everything provisioned in your AWS or GCP account.
  • Axum for most new APIs, especially when you want Tower middleware and compile-time checked state.
  • Actix Web when you want many first-party features and a long track record.
  • Rocket when concise routes and request guards matter most.

Rust support in Encore is in beta, and you can sign up for access if you want to try it with the framework you already use.

Frequently asked questions

Should I use Axum, Actix Web or Rocket?

Axum is the usual default for new projects because it builds on Tokio, Hyper and Tower and reuses their middleware ecosystem. Actix Web fits teams that want a mature framework with many first-party features, and Rocket fits teams that prefer concise macro-driven routes and request guards.

Is Actix Web faster than Axum?

Actix Web often scores slightly higher in synthetic benchmarks, but both handle far more requests than a typical API needs, and database and network calls dominate real response times.

Does Rocket support async?

Yes. Rocket 0.5 runs on stable Rust and handlers can be async functions running on Tokio.

Can I use Tower middleware with Actix Web or Rocket?

Not directly. Tower layers plug into Axum and other Hyper-based frameworks, while Actix Web has its own middleware trait and Rocket uses fairings.

How does Encore fit with these frameworks?

Encore is an infrastructure SDK you add to whichever of these frameworks you pick, with Axum as the example here. The framework keeps routing and extraction, and Postgres databases, Pub/Sub topics, buckets and cron jobs are static declarations in the crate. encore run starts the database, topics and buckets locally, cron jobs run only where the app is deployed, and a deploy provisions everything in your own AWS or GCP account. Rust support is in beta.

How do I add Postgres and Pub/Sub to an Axum app?

Add the encore-dev crate to the existing Axum service, call encore::initialize() before serving, and declare Database::new and Topic::new as static values that handlers use directly. encore run starts them locally and a deploy provisions them in your own AWS or GCP account, and the same SDK works with whichever framework you pick.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders