Updated April 2026. The Rust serverless toolchain has matured considerably since the original 2019 version of this post. The cross-compilation steps, the hand-written
Makefile, and the runtime versions described in the original are all out of date. What follows reflects the current, minimal setup.
When this post first appeared in 2019, deploying Rust to AWS Lambda involved cross-compiling against musl-libc, packaging the binary by hand, and persuading SAM's build system to invoke a custom Makefile. The motivation was reasonable: Rust's binary size, cold-start latency, and predictable runtime behaviour map well onto Lambda's per-request invocation model. The integration, however, was rough.
The toolchain has since been consolidated. AWS now endorses cargo-lambda, a Cargo subcommand that handles cross-compilation, packaging, and deployment. The runtime crates (lambda_runtime, lambda_http) have stabilised around an API that maps cleanly onto tower's Service trait. The current minimal setup fits in roughly a screenful of code with no HTTP framework involved. lambda_http adapts the Lambda event into a tower::Service<Request> and the rest is plain Rust.
What changed since 2019
| 2019 | 2026 |
|---|---|
provided runtime (Amazon Linux 1) | provided.al2023 (AL1 deprecated late 2023) |
lambda_runtime = "0.2" | lambda_runtime = "0.13" |
lambda_http = "0.1" | lambda_http = "0.13" |
tokio = "0.3" | tokio = "1" |
Manual musl-cross cross-compile | cargo lambda build |
Hand-written Makefile for SAM | cargo lambda deploy |
Project setup
cargo install cargo-lambda
cargo lambda new rust-aws
cd rust-aws
cargo lambda new scaffolds a project targeting provided.al2023 with the runtime crates wired up. A minimal Cargo.toml:
[package]
name = "rust-aws"
version = "0.1.0"
edition = "2021"
[dependencies]
lambda_http = "0.13"
lambda_runtime = "0.13"
tokio = { version = "1", features = ["macros"] }
tower = "0.5"
tracing = "0.1"
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] }
The simplest viable handler dispatches on method and path inside a single service_fn. service_fn lifts an async function into a tower::Service, which is the only thing lambda_http::run requires:
use lambda_http::{run, service_fn, Body, Error, Request, Response};
async fn handle(req: Request) -> Result<Response<Body>, Error> {
let response = match (req.method().as_str(), req.uri().path()) {
("POST", "/comment") => Response::builder().status(200).body("comment ok".into())?,
("POST", "/contact") => Response::builder().status(200).body("contact ok".into())?,
_ => Response::builder().status(404).body(Body::Empty)?,
};
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.with_target(false)
.without_time()
.init();
run(service_fn(handle)).await
}
For cross-cutting concerns (timeouts, structured logging, retries, concurrency limits), wrap the service with tower::ServiceBuilder rather than reaching for a framework:
use std::time::Duration;
use tower::ServiceBuilder;
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt().init();
let svc = ServiceBuilder::new()
.timeout(Duration::from_secs(10))
.service(service_fn(handle));
run(svc).await
}
There is no HTTP server running inside the Lambda environment; each invocation drives a single request through the service and exits.
Building and deploying
cargo-lambda handles cross-compilation:
cargo lambda build --release
The output binary is written to target/lambda/rust-aws/bootstrap, the filename the provided.* runtimes expect. To deploy in one step:
cargo lambda deploy
For SAM-based pipelines, build the artifact separately and reference it from the template:
cargo lambda build --release --output-format zip
Resources:
RustAws:
Type: AWS::Serverless::Function
Properties:
Handler: bootstrap
Runtime: provided.al2023
Architectures: [arm64]
MemorySize: 128
Timeout: 10
CodeUri: target/lambda/rust-aws/
Events:
Comment:
Type: HttpApi
Properties: { Path: /comment, Method: post }
Contact:
Type: HttpApi
Properties: { Path: /contact, Method: post }
Two changes from the 2019 template are worth flagging:
HttpApi(API Gateway v2) is preferred over the originalApi(REST) integration: lower per-request latency and cost, andlambda_httpparses both event formats transparently.arm64(Graviton) reduces compute cost by roughly 20% relative tox86_64at equivalent memory configurations. Rust binaries cross-compile toaarch64-unknown-linux-gnuwithout modification.
Cold-start behaviour
The original motivation for choosing Rust was cold-start latency. A bare service_fn handler initialises in roughly 20–40 ms on a 128 MB function configured for arm64. Heavy dependencies (full HTTP frameworks, large macro-derived schemas, eager lazy_static initialisation) push initialisation into the hundreds of milliseconds and erode Rust's advantage. Keeping the dependency graph small is the only reliable lever.
Lambda SnapStart, AWS's snapshot-based cold-start mitigation, is generally available for managed runtimes but not for provided.al2023 at the time of writing. This is the one remaining ergonomic gap for Rust on Lambda.
Migrating from the 2019 setup
For an existing project built against the original instructions:
- Replace the
Makefileandmusl-crosstoolchain withcargo lambda build. - Update
lambda_runtime,lambda_http, andtokioto current major versions. - Replace
Runtime: providedwithRuntime: provided.al2023in the SAM template. - Switch new endpoints to
HttpApi. ExistingApiendpoints can stay if breaking the URL is undesirable.
A reference implementation is maintained at the cargo-lambda examples.