Group
← Back

AWS Lambda + Rust

March 27, 2019RustAWSDevOpsServerless

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

20192026
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-compilecargo lambda build
Hand-written Makefile for SAMcargo 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 original Api (REST) integration: lower per-request latency and cost, and lambda_http parses both event formats transparently.
  • arm64 (Graviton) reduces compute cost by roughly 20% relative to x86_64 at equivalent memory configurations. Rust binaries cross-compile to aarch64-unknown-linux-gnu without 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:

  1. Replace the Makefile and musl-cross toolchain with cargo lambda build.
  2. Update lambda_runtime, lambda_http, and tokio to current major versions.
  3. Replace Runtime: provided with Runtime: provided.al2023 in the SAM template.
  4. Switch new endpoints to HttpApi. Existing Api endpoints can stay if breaking the URL is undesirable.

A reference implementation is maintained at the cargo-lambda examples.