Axum, Actix Web, Rocket and Loco compared for teams building Rust backends, plus how Encore adds databases, Pub/Sub and cloud infrastructure to whichever framework you pick, such as Axum.
Rust web development has largely settled on Axum for new APIs, with Actix Web and Rocket as the established alternatives and Loco offering a Rails-style project on top of Axum. All of them run on Tokio and handle routing, extraction and serialization well, but once the service needs a Postgres database, a message broker and a bucket, the framework leaves those to you and the team ends up writing Docker Compose files for local development and Terraform for production next to the Rust code.
What to weigh when choosing:
Encore combines the Axum service you already have with infrastructure declared as static values in the same crate, as an SDK you add with the encore-dev crate and an encore::initialize() call rather than a framework you switch to, so Axum keeps handling routing and extraction while Postgres databases, Pub/Sub topics and subscriptions, object storage buckets, Redis, cron jobs and secrets are declarations that Encore finds through static analysis and provisions. encore run starts the databases, topics and buckets locally and applies migrations, while cron jobs run only where the app is deployed, and a deploy provisions the same declarations in your own AWS or GCP account, such as RDS and SNS/SQS on AWS or Cloud SQL and Pub/Sub on GCP.

An Axum handler that inserts a row and publishes an event uses the declarations directly, without a pool in State or a broker client to construct at startup:
use axum::{http::StatusCode, routing::post, Json, Router};
use encore::pubsub::Topic;
use encore::storage::postgres::Database;
use serde::{Deserialize, Serialize};
// Encore finds these declarations and provisions them
// locally, in preview environments, and in your cloud.
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();
}
A subscription to that topic is another static, registered with #[encore::pubsub::register] so it starts with the app:
use encore::pubsub::{Message, Subscription};
#[encore::pubsub::register]
static WELCOME: Subscription<Signup> =
Subscription::new(&SIGNUPS, "send-welcome", send_welcome).max_concurrency(4);
async fn send_welcome(msg: Message<Signup>) -> Result<(), encore::Error> {
send_email(&msg.data.email).await
}
Scheduled work is declared the same way, and the route that serves the job is built from the job's own path so the two cannot drift apart:
use encore::cron;
// Encore calls POST /internal/welcome-emails every 2 hours.
static WELCOME_EMAILS: cron::Job = cron::Job::new(
"welcome-email",
"/internal/welcome-emails",
cron::Schedule::Every("2h"),
)
.title("Send welcome emails");
let app = Router::new()
.route(WELCOME_EMAILS.path(), post(send_welcome_emails));
static declarations next to the code that uses them.static in the same change, and the next deploy provisions it like the rest of the service.encore run starts the infrastructure locally, and under encore test each test gets its own migrated database.Encore handles infrastructure, leaves HTTP to the framework and works with whichever one you pick, so the framework comparison below still applies. Cron jobs run where the app is deployed rather than under encore run. Rust support is in beta, and you can sign up for access.
AxumAxum is maintained by the Tokio project and builds on Hyper and Tower, so handlers are plain async functions whose arguments are extractors and any Tower middleware works as a layer.
use axum::{http::StatusCode, routing::post, Json, Router};
use serde::Deserialize;
#[derive(Deserialize)]
struct Signup {
email: String,
}
async fn create_user(Json(req): Json<Signup>) -> (StatusCode, String) {
(StatusCode::CREATED, req.email)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/users", post(create_user));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
State.Axum is a router and leaves the database, auth, background jobs and project structure to you, and handler trait errors can be cryptic when an extractor is in the wrong position, and the #[debug_handler] macro from axum-macros helps there. Route path syntax changed in 0.8 from /:id to /{id}.
Actix WebActix Web is one of the oldest production Rust frameworks and pairs attribute macros for routes with typed extractors.
use actix_web::{post, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;
#[derive(Deserialize)]
struct Signup {
email: String,
}
#[post("/users")]
async fn create_user(req: web::Json<Signup>) -> impl Responder {
HttpResponse::Created().body(req.email.clone())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(create_user))
.bind(("0.0.0.0", 8080))?
.run()
.await
}
web::Data state.Actix Web runs each worker on its own single-threaded Tokio runtime, so some Tokio-based crates and Send bounds behave differently than in Axum, and its middleware model is its own rather than Tower, which means fewer shared crates with the Axum ecosystem.
RocketRocket uses attribute macros and request guards so that a route declaration carries its method, path and data format, and since 0.5 it runs on stable Rust with async handlers.
#[macro_use]
extern crate rocket;
use rocket::http::Status;
use rocket::serde::{json::Json, Deserialize};
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Signup {
email: String,
}
#[post("/users", data = "<req>")]
fn create_user(req: Json<Signup>) -> (Status, String) {
(Status::Created, req.email.clone())
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![create_user])
}
rocket_db_pools are built in or first-party.Rocket releases less often than Axum or Actix Web, and its macro-heavy style can make it harder to step outside the patterns it expects, so teams starting large projects usually compare its current release activity with the alternatives.
Loco generates a full project around Axum with SeaORM models, migrations, background workers, mailers and authentication, and its CLI scaffolds controllers and models in the style of Rails.
Loco makes many decisions for you, which speeds up the first weeks and constrains you later if your needs differ from its conventions, and the database, Redis and worker processes still have to be run locally and provisioned for production.
Whichever framework you pick, the database, broker and buckets still have to exist locally, in preview environments and in production. Rust support in Encore is in beta, and you can sign up for access if you want to declare that infrastructure in the framework you already use.
Axum is the common default for new APIs because it is built on Tokio, Tower and Hyper. Actix Web is a mature, fast alternative with its own ecosystem, Rocket favors ergonomics and built-in features, and Loco gives you a Rails-like full-stack structure on top of Axum. Encore is not a competing framework but an infrastructure SDK you add to whichever framework you pick, such as Axum, and it provisions the infrastructure the code declares.
Neither is faster in a way that matters for most APIs. Axum integrates directly with the Tokio and Tower ecosystem and uses plain async functions with extractors, while Actix Web has its own runtime layer on top of Tokio, a larger built-in feature set and a long production track record.
Rocket 0.5 is stable on Rust stable and supports async, although releases come less often than for Axum or Actix Web, so check the current release activity before starting a large project on it.
Add the encore-dev crate to the Axum service you already have, call encore::initialize() before serving, and declare Database::new and Topic::new as static values, with Subscription::new for subscribers marked #[encore::pubsub::register] so that encore::initialize() starts them. Axum keeps the routing, encore run starts the database and topic locally, and a deploy provisions them in your own AWS or GCP account.
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.