Running a Rust Axum API on AWS with Encore, ECS on Fargate, AWS Lambda or EC2, including Docker builds, databases, secrets and what each option leaves for you to manage.
Axum compiles to one static binary, which makes the application side of deploying to AWS simple, and most of the work is in what surrounds the binary, such as an image registry, a compute service, a load balancer, the VPC it runs in, a database with credentials and logs you can read. This guide compares Encore, ECS on Fargate, Lambda and EC2 for an Axum API, with code for each.
The 2024 State of Rust Survey found Rust especially popular for server backends, web and networking services and cloud technologies. Rust was again the most admired programming language in the 2025 Stack Overflow Developer Survey, at 72%.
What you need to decide:
Encore combines the Axum service and the infrastructure it declares into one deploy to your own AWS account. You add the encore-dev crate to the Axum app you have, routing stays in Axum, and resources are static declarations that Encore finds through static analysis. It is an infrastructure SDK, not a replacement for Axum.
use axum::{extract::Path, http::StatusCode, routing::{get, post}, Json, Router};
use encore::storage::postgres::Database;
use serde::Serialize;
// Provisioned locally by encore run and in your AWS account on deploy.
static DB: Database = Database::new("users").migrations("./migrations");
#[derive(Serialize)]
struct User {
id: i32,
name: String,
}
async fn add_user(Path(name): Path<String>) -> StatusCode {
match DB.execute("INSERT INTO users (name) VALUES ($1)", &[&name]).await {
Ok(_) => StatusCode::CREATED,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
async fn get_user(Path(id): Path<i32>) -> Result<Json<User>, StatusCode> {
let row = DB
.query_opt("SELECT id, name FROM users WHERE id = $1", &[&id])
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(User { id: row.get("id"), name: row.get("name") }))
}
#[tokio::main]
async fn main() {
encore::initialize();
let app = Router::new()
.route("/users/:name", post(add_user))
.route("/users/id/:id", get(get_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();
std::process::exit(encore::shutdown::drained().await.code());
}
/users/{name}.An encore.service.json next to Cargo.toml names the service and exposes its HTTP API through the gateway:
{
"name": "users",
"lang": "rust",
"run": { "type": "rust" },
"api": [
{ "type": "http", "prefix": "/", "name": "users", "expose": true }
]
}
encore run starts Postgres locally, applies the migrations and starts the binary. After you connect your AWS account, Encore provisions the infrastructure in that account on deploy, including the database the code declares.
The setup suits agents working on the repo, since the agent can add a resource and the handler that uses it in one diff and run it locally, while cloud access sits with Encore's IAM role, not the agent.
Without Encore the Axum code is the same apart from the static declarations, and the AWS side is built by hand or with an infrastructure-as-code tool: the ECR repository, the cluster and service, the load balancer, the RDS instance and its security group, the secret holding the password and the task role allowed to read it. The ECS section below shows that route.
Fargate runs containers without servers to patch, and a multi-stage Dockerfile keeps the image small by compiling in one stage and shipping only the binary:
FROM rust:1.90 AS build
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/release/my-api /usr/local/bin/my-api
USER 1000
EXPOSE 8080
CMD ["my-api"]
The image is pushed to ECR and referenced by a task definition:
ECS Express Mode creates the Fargate service, load balancer and scaling from an image, which shortens the first deploy. The database, its secret and the task role the container uses to read it are still separate resources you create.
The lambda_http crate runs an Axum Router as a Lambda function, since a router is a tower service:
use axum::{routing::get, Router};
use lambda_http::{run, Error};
async fn health() -> &'static str {
"ok"
}
#[tokio::main]
async fn main() -> Result<(), Error> {
let app = Router::new().route("/health", get(health));
run(app).await
}
Rust needs no managed runtime on Lambda because it compiles to native code and runs on an OS-only runtime, and Cargo Lambda builds and deploys the function.
A function runs for at most 15 minutes, and a request through an API Gateway REST API has an integration timeout of 50 milliseconds to 29 seconds, which can be raised for Regional and private APIs at the cost of a lower throttle quota. Many concurrent functions opening Postgres connections usually call for RDS Proxy.
Copying the binary to an EC2 instance and running it under systemd behind a load balancer is the most manual route, which suits teams that already operate instances and leaves patching, scaling and deploys to you.
Rust support in Encore is in beta, and you can sign up for access to deploy an Axum app to your own AWS account.
For a single container, ECS on Fargate with a multi-stage Docker build is the most common route. Lambda with the lambda_http crate suits small APIs with spiky traffic. Encore fits if the app also needs Postgres or other infrastructure, since a deploy provisions the services and resources in your own AWS account from the Rust code.
Use a multi-stage Dockerfile: compile with cargo build --release in a rust image, then copy the binary into a slim runtime image such as debian:bookworm-slim or a distroless image, so the final image contains only the binary and its runtime libraries.
Yes. The lambda_http crate adapts a tower service such as an Axum Router to Lambda events, and the function is deployed on an OS-only runtime because Rust compiles to a native binary. Cold starts are short for Rust, but a request through an API Gateway REST API has an integration timeout of at most 29 seconds unless you request a higher quota for a Regional API.
Create the RDS instance in the same VPC, store the password in Secrets Manager, allow the service's security group in the database security group and pass the connection details to the container. With Encore, you declare Database::new in your code and Encore provisions the database in your AWS account on deploy.
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.