Lambda
A Lambda function whose role can only write logs until you grant more. Graviton, one-year log retention and IAM-only function URLs by default.
Choosing a runtime
Set runtime and point handler at your code. Anvil builds the code before every anvil deploy. Everything else about the function (the least-privilege role, the log group,
grants and hatch) is the same whichever runtime you pick.
runtime | Write it in | handler |
|---|---|---|
nodejs22+ | TypeScript or JavaScript on Node.js 22 or newer | functions/ |
rust | Rust, built with cargo‑lambda | functions/ |
Every runtime runs on arm64 (Graviton) unless you set architecture: 'x86_64'.
Node.js
Node.js 22 and newer: set runtime to nodejs24.x (recommended) or nodejs22.x. The handler is the entry file
without its extension, plus the name of the function it exports. Omit handler to deploy a blank
placeholder that returns 200.
const api = new anvil.aws.Lambda('api', {
runtime: 'nodejs24.x',
handler: 'functions/api/index.handler',
});api = anvil.aws.Lambda("api",
runtime="nodejs24.x",
handler="functions/api/index.handler",
)api, err := anvilaws.NewLambda(ctx.PulumiCtx(), "api", &anvilaws.LambdaArgs{
Runtime: anvilaws.LambdaRuntime_Nodejs24_x,
Handler: pulumi.String("functions/api/index.handler"),
})
if err != nil {
return err
}// functions/api/index.ts
export const handler = async (event: unknown) => {
return { statusCode: 200, body: JSON.stringify({ ok: true }) };
};How it's built
- One file. esbuild bundles the entry and everything it imports into a single
index.js, so nonode_modulesships. Prefer pure-JS packages: native addons can't be bundled. - TypeScript without a compile step. esbuild strips types but doesn't check them, so run
tsc --noEmitin CI to catch type errors. - Skipped when nothing changed. A function is rebuilt only when a file in its entry's folder
changes.
anvil deploy --force-cacheskips the build and reuses the last one.
Rust
The handler is a Cargo package directory. You don't create it: anvil generate, or the next anvil deploy, writes the package the first time it sees the function.
const api = new anvil.aws.Lambda('api', {
runtime: 'rust',
handler: 'functions/api',
});api = anvil.aws.Lambda("api",
runtime="rust",
handler="functions/api",
)api, err := anvilaws.NewLambda(ctx.PulumiCtx(), "api", &anvilaws.LambdaArgs{
Runtime: anvilaws.LambdaRuntimeRust,
Handler: pulumi.String("functions/api"),
})
if err != nil {
return err
}What gets created
$ anvil generate
created Cargo.toml (workspace)
created rust-toolchain.toml
created .gitignore (+ /target)
created functions/api/- One Cargo workspace at the project root, shared by every Rust Lambda. If you already have one, new packages join it.
- Create only. Existing packages are never edited, and removing a function never deletes its code.
- Named after the function:
new Lambda('ordersApi', …)becomes the packageorders-api, whatever its directory is called. - Commit
Cargo.lock.target/is added to.gitignore.
Creating the package yourself
You rarely need to. Declaring the function and running anvil generate does all of this for you,
plus the shared workspace and the pinned toolchain. Create it by hand only when you want to set the package
up your own way.
Anvil only creates what's missing: if a package already exists at the handler path, it's built as
it is. Create it before the next anvil generate or anvil deploy, from the project root:
cargo new --bin functions/api
cd functions/api
cargo add lambda_runtime tokio serde_json --features tokio/macrosReplace src/main.rs with a handler that calls lambda_runtime::run, such as the one
below, and deploy. A few things to keep in mind:
- Inside an existing Cargo workspace,
cargo newadds the package to itsmembers. Outside one, it's a standalone package with its ownCargo.lock, which works too. - No
rust-toolchain.tomlmeans your default Rust is used. Edition 2024 needs 1.85 or newer. - One binary per package, or point
handlerat the binary's.rsfile.
The handler
Every new function starts from a blank async handler: JSON in, JSON out. Swap in event types from aws_lambda_events or lambda_http when you know what invokes it.
use lambda_runtime::{run, service_fn, tracing, Error, LambdaEvent};
use serde_json::{json, Value};
async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
let (_payload, _context) = event.into_parts();
Ok(json!({ "ok": true }))
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
// Create SDK clients and DB connections here, once per cold start,
// and pass them into the handler.
run(service_fn(handler)).await
}A package with several binaries needs the binary named: point handler at its source file
instead, e.g. functions/. Renamed [[bin]] targets work too.
How it's built
- One build per workspace.
cargo lambda build --releaseruns once for all your Rust functions, so shared crates compile once. A function only redeploys when its binary changed. - You need rustup and either cargo‑lambda 1.9.2 or newer (from its installation page, not crates.io) or Docker, which runs the build in the official cargo‑lambda image.
- Rust is pinned in
rust-toolchain.toml, so your laptop and CI use the same compiler. The file is yours: changechannelto upgrade.
[toolchain]
channel = "1.99.1"
components = ["rust-analyzer", "rustfmt", "clippy"]
targets = ["aarch64-unknown-linux-gnu", "x86_64-unknown-linux-gnu"]cargo check, cargo clippy and cargo test work as usual; plain cargo build targets your machine, not Lambda. To run a function locally, start the emulator, then
invoke it from a second terminal:
cargo lambda watch
cargo lambda invoke api --data-ascii '{}'