Axum, Actix Web and tonic compared for Rust services that call each other and exchange events, plus how Encore adds per-service databases, Pub/Sub and cloud infrastructure to the framework you pick.
Rust services are usually small, fast and long-running, which makes the language a good fit for the parts of a system that sit on the hot path, and the 2024 State of Rust Survey found that 38 percent of respondents now use Rust for the majority of their coding at work, up from 34 percent a year earlier.
The Rust web ecosystem has converged on Tokio, and the main framework choices for services are Axum, Actix Web and tonic for gRPC, although none of them decides how services find each other, how events travel between them, or where each service's database runs.
What to weigh when choosing:
AxumAxum handlers are async functions whose arguments are extractors, and any Tower layer works as middleware, which includes the same tracing and timeout layers tonic uses.
Axum is a router, so service discovery, brokers and databases are left to you, and route path syntax changed in 0.8 from /:id to /{id}.
Actix WebActix Web has a long production history and strong benchmark results, with attribute macros for routes and its own middleware model.
web::Data state.Its middleware is not Tower, so fewer crates are shared with Axum and tonic services in the same system.
tonictonic generates clients and servers from .proto files with tonic-build and supports unary and streaming RPCs over HTTP/2.
Code generation adds a build step for each contract change, and browsers need gRPC-Web or a proxy to call tonic services.
Encore combines each Rust service with its own infrastructure through an infrastructure SDK you add with the encore-dev crate, so Axum keeps the routing while each service declares its Postgres database, Pub/Sub topics, buckets, cron jobs and secrets as static values that Encore finds through static analysis. encore run starts Postgres locally with the topics next to the services, and a deploy provisions the same declarations in your own AWS or GCP account. Whichever option above you pick, Encore can add the databases, topics and buckets to the same services.
An orders service checks the user through the users service, writes the order and publishes an event:
use axum::{extract::Path, http::StatusCode, routing::post, Json, Router};
use encore::pubsub::Topic;
use encore::storage::postgres::Database;
use serde::{Deserialize, Serialize};
static DB: Database = Database::new("orders").migrations("./migrations");
static ORDER_PLACED: Topic<OrderPlaced> = Topic::new("order-placed");
#[derive(Debug, Clone, Serialize, Deserialize)]
struct OrderPlaced {
order_id: i64,
user_id: i64,
}
async fn place_order(Path(user_id): Path<i64>) -> Result<Json<OrderPlaced>, StatusCode> {
// Encore resolves the users service's address in every environment.
let users = encore::services::base_url("users");
reqwest::get(format!("{users}/users/{user_id}"))
.await
.and_then(|r| r.error_for_status())
.map_err(|_| StatusCode::BAD_GATEWAY)?;
let row = DB
.write()
.query_one("INSERT INTO orders (user_id) VALUES ($1) RETURNING id", &[&user_id])
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let event = OrderPlaced { order_id: row.get(0), user_id };
ORDER_PLACED
.publish(&event)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(event))
}
#[tokio::main]
async fn main() {
encore::initialize();
let app = Router::new().route("/orders/:user_id", post(place_order));
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 notifications service references the topic by name and registers a subscription that starts with the service:
use encore::pubsub::{Message, Subscription, Topic};
static ORDER_PLACED: Topic<OrderPlaced> = Topic::named("order-placed");
#[encore::pubsub::register]
static RECEIPTS: Subscription<OrderPlaced> =
Subscription::new(&ORDER_PLACED, "send-receipt", send_receipt).max_retries(3);
async fn send_receipt(msg: Message<OrderPlaced>) -> Result<(), encore::Error> {
println!("sending receipt for order {}", msg.data.order_id);
Ok(())
}
base_url are read from the code, which gives Encore the dependency graph between services.encore run before the change reaches review.Encore leaves HTTP and gRPC to your framework, so the comparison below still applies. When Encore deploys to your cloud, the cloud access sits with Encore's IAM role, not the agent or the developer. Rust support is in beta, and you can sign up for access.
The Twelve-Factor App advice to treat databases and brokers as attached resources, swappable through config, applies to Rust services as much as any. For calls between services, tonic runs over HTTP/2, whose streams let many requests share one connection. Encore's preview environments give each pull request its own environment with real infrastructure on paid plans. Rust support in Encore is in beta, and you can sign up for access.
Axum is the common default for HTTP services because it builds on Tokio, Tower and Hyper, and tonic is the standard choice for gRPC and shares the same Tower middleware. Actix Web is a mature alternative with its own ecosystem. Encore is not a competing framework but an infrastructure SDK you add to the framework you pick, such as Axum, giving each service its own Postgres database and Pub/Sub topics.
Use tonic and gRPC when services in several languages share Protobuf contracts or need streaming RPCs. Use Axum with JSON when the services are called from browsers or other teams prefer plain HTTP. Axum and tonic can be served from the same process.
Call encore::services::base_url with the other service's name as a string literal and send the request with an HTTP client such as reqwest. Encore reads the call from the code to record the dependency, and resolves the URL under encore run and in each deployed environment.
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.