Actix-web
Actix-web is a mature, high-performance HTTP framework with its own runtime model - a strong alternative to Axum for teams that want it.
Search across all documentation pages
Actix-web is a mature, high-performance HTTP framework with its own runtime model - a strong alternative to Axum for teams that want it.
Quick-reference recipe card - copy-paste ready.
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateItem { name: String }
#[get("/health")]
async fn health() -> impl Responder { "ok" }
#[post("/items")]
async fn create(body: web::Json<CreateItem>) -> HttpResponse {
HttpResponse::Created().json(serde_json::json!({ "name": body.name }))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(health).service(create))
.bind(("127.0.0.1", 8080))?
.run()
.await
}When to reach for this: You need a battle-tested framework with built-in extractors, testing utilities, and a large ecosystem - and your team is comfortable outside the Tower-only stack.
use actix_web::{delete, get, post, web, App, HttpResponse, HttpServer};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Clone)]
struct Item { id: u64, name: String }
#[derive(Deserialize)]
struct NewItem { name: String }
struct AppState { items: Mutex<Vec<Item>> }
#[get("/items")]
async fn list(data: web::Data<AppState>) -> web::Json<Vec<Item>> {
web::Json(data.items.lock().unwrap().clone())
}
#[post("/items")]
async fn create(data: web::Data<AppState>, body: web::Json<NewItem>) -> HttpResponse {
let mut items = data.items.lock().unwrap();
let item = Item { id: items.len() as u64 + 1, name: body.name.clone() };
items.push(item.clone());
HttpResponse::Created().json(item)
}
#[delete("/items/{id}")]
async fn remove(data: web::Data<AppState>, path: web::Path<u64>) -> HttpResponse {
let mut items = data.items.lock().unwrap();
if let Some(pos) = items.iter().position(|i| i.id == *path) {
items.remove(pos);
HttpResponse::NoContent().finish()
} else {
HttpResponse::NotFound().finish()
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let state = web::Data::new(AppState { items: Mutex::new(vec![]) });
HttpServer::new(move || {
App::new()
.app_data(state.clone())
.service(list)
.service(create)
.service(remove)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}What this demonstrates:
#[get], #[post] define routes on handler functions.web::Data<T> shares state across worker threads.web::Json for request and response serialization.HttpServer spawns multiple workers for parallelism.HttpServer clones the App factory per worker thread.web::Path, web::Query, web::Json mirror Axum ergonomics.| Topic | Axum | Actix-web |
|---|---|---|
| Ecosystem | Tower layers everywhere | Actix middleware |
| State | State<T> + Clone | web::Data<T> + Arc |
| Learning curve | Tower concepts | Self-contained docs |
| Performance | Excellent | Excellent |
// Scope routes under a prefix
use actix_web::web::scope;
App::new().service(scope("/api/v1").service(list).service(create));std::sync::Mutex across await points can deadlock. Fix: Use tokio::sync::Mutex or keep lock scopes short.HttpServer::new closure runs per worker. Fix: Create shared web::Data outside the closure.sqlx, reqwest).actix-web middleware or wrap at reverse proxy.TestRequest API differs from Axum oneshot. Fix: Standardize on one framework per service.| Alternative | Use When | Don't Use When |
|---|---|---|
| Axum | New projects wanting Tower composability | Team has deep Actix expertise and existing code |
| Rocket | Rapid prototyping with macros | Production services needing max ecosystem choice |
| warp | Legacy hyper 0.14 stacks | Greenfield projects |
Yes - active releases; widely used in production.
No for typical HTTP handlers - async functions suffice.
actix-web-actors and actix-ws support upgrades.
Works via web::Data<PgPool> and #[get] handlers.
Same as Axum - reverse proxy to bound port, pass X-Forwarded-* headers.
paperclip or manual schemas - not built-in.
HttpServer::run handles signals; configure timeout for drain.
Rewrite routes and middleware; share domain logic crates unchanged.
Match worker count to CPU cores; profile before micro-optimizing framework choice.
Default new services to Axum; keep Actix when migration cost exceeds benefit.
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.
Reviewed by Chris St. John·Last updated Jul 16, 2026