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:
Router and plain async functions, while Actix Web usually declares them with attribute macros such as #[post].Transform trait.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 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.
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.
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.
Either works with Encore when you want Postgres and Pub/Sub declared in Rust and provisioned in your own cloud account.
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.
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.
Not directly. Tower layers plug into Axum and other Hyper-based frameworks, while Actix Web uses its own Transform trait for middleware.
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.
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.