Oct 23, 20265 min read

Axum vs Actix Web

How Axum and Actix Web compare on routing, extractors, middleware, runtime model and ecosystem, with the same endpoint written in each.

Axum and Actix Web are the two Rust web frameworks most teams shortlist for a new API. Both run async handlers on Tokio and use Serde for JSON. They differ in how routes are declared, how middleware is written and which crates they share code with, and those differences matter more as the service grows.

The main differences:

  • Axum declares routes with a Router and plain async functions, while Actix Web usually declares them with attribute macros such as #[post].
  • Axum uses Tower layers for middleware, while Actix Web has its own Transform trait.
  • Actix Web starts one single-threaded runtime per worker, while Axum runs on a multi-threaded Tokio runtime.
  • Actix Web ships first-party crates for sessions, CORS, multipart and static files, while Axum relies on the wider Tower and Hyper ecosystem.

Axum vs Actix Web at a glance

CapabilityEncoreAxumActix Web
RoleInfrastructure SDK added to either frameworkWeb frameworkWeb framework
Route declarationFrom the frameworkRouter::new().route(...)#[post(...)] macros or App::route
Request parsingFrom the frameworkExtractors such as Json and StateExtractors such as web::Json and web::Data
Shared stateDeclared as a staticState via with_stateweb::Data via app_data
MiddlewareFrom the frameworkTower layersActix Transform
Runtime modelFrom the frameworkMulti-threaded TokioSingle-threaded runtime per worker

Axum: functions and extractors

An Axum handler is an async function whose arguments are extractors. The Axum 0.7 documentation describes how the router checks at compile time that each handler's state matches what the router provides. This signup endpoint takes a SQLx pool through State:

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(()) }

Compiler errors for a handler with extractors in the wrong order can be hard to read, and #[debug_handler] from axum-macros improves them. Axum 0.8 changed path parameters from /:id to /{id}, so check which version an example targets.

Actix Web: macros and workers

Actix Web starts one worker per available CPU thread by default, and the Actix server documentation explains that each worker gets its own application instance. Shared state therefore goes through web::Data, which wraps it in an Arc:

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 }

Forgetting to register app_data compiles fine and fails at request time with a 500, which is a common runtime surprise for new Actix users.

Performance

Actix Web has long ranked among the fastest frameworks in the TechEmpower benchmarks, and Axum usually lands close to it. Both handle far more traffic than most APIs see, so database queries and serialization decide real latency, and ergonomics and ecosystem fit matter more.

Adding infrastructure to either framework with Encore

Encore combines the Rust service you already have with infrastructure declared as static values in the same crate. It is an infrastructure SDK added as the encore-dev crate, not a framework you switch to. Postgres databases, Pub/Sub topics, object storage buckets, Redis, cron jobs and secrets become declarations that Encore discovers and provisions, and the handlers use them without a pool in State.

The signup endpoint with Encore, written for Axum 0.7:

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(); }

The DB and SIGNUPS statics do not depend on the router, so an Actix Web handler can call DB.execute and SIGNUPS.publish the same way.

encore run starts Postgres locally and applies the migrations, along with the topic and any buckets, and the CLI reference covers encore test, which gives each test its own migrated database.

A deploy provisions the declarations in your own AWS or GCP account.

A coding agent that needs a queue for a new handler writes the Topic::new static in the same diff and runs it under encore run before anyone reviews it. Cloud access sits with Encore's IAM role, not the agent, and the AI integration guide explains how to give an agent Encore's conventions.

Rust support is in beta, and you can request access.

Try Encore →

How to choose

  • Axum for most new APIs, especially when you want Tower middleware and compile-time checked state.
  • Actix Web when you want many first-party crates and a long production record.

Either works with Encore when you want Postgres and Pub/Sub declared in Rust and provisioned in your own cloud account.

Frequently asked questions

Should I use Axum or Actix Web?

Axum is the usual default for new projects because it builds on Tokio and Tower and reuses Tower middleware. Actix Web suits teams that want a mature framework with many first-party crates and its own middleware model.

Is Actix Web faster than Axum?

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

Can I use Tower middleware with Actix Web?

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

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

Add the encore-dev crate, call encore::initialize() before serving, and declare Database::new and Topic::new as static values that handlers use directly. encore run starts Postgres and the topic locally, and a deploy provisions them in your own AWS or GCP account.

Which is easier for an AI coding agent to work in?

Both are well represented in training data. Axum handlers are plain functions, so an agent can read the router to see every route, while Actix Web routes are declared with attribute macros and registered with App::service.

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