Note
The Rust runtime client
The Lambda function handler is the method in your function code that processes events. When your function is invoked, Lambda runs the handler method. Your function runs until the handler returns a response, exits, or times out.
This page describes how to work with Lambda function handlers in Rust, including project initialization, naming conventions, and best practices. This page also includes an example of a Rust Lambda function that takes in information about an order, produces a text file receipt, and puts this file in an Amazon Simple Storage Service (S3) bucket. For more information about how to deploy your function after writing it, see Deploy Rust Lambda functions with .zip file archives.
Topics
Setting up your Rust handler project
When working with Lambda functions in Rust, the process involves writing your
code, compiling it, and deploying the compiled artifacts to Lambda. The simplest way
to set up a Lambda handler project in Rust is to use the
AWS Lambda Runtime for Rustlambda_runtime
) that supports writing Lambda functions in Rust and interfacing
with AWS Lambda's execution environment.
Use the following command to install the AWS Lambda Runtime for Rust:
cargo install cargo-lambda
After you successfully install cargo-lambda
, use the following command
to initialize a new Rust Lambda function handler project:
cargo lambda new example-rust
When you run this command, the command line interface (CLI) asks you a couple of questions about your Lambda function:
-
HTTP function – If you intend to invoke your function via API Gateway or a function URL, answer Yes. Otherwise, answer No. In the example code on this page, we invoke our function with a custom JSON event, so we answer No.
-
Event type – If you intend to use a predefined event shape to invoke your function, select the correct expected event type. Otherwise, leave this option blank. In the example code on this page, we invoke our function with a custom JSON event, so we leave this option blank.
After the command runs successfully, enter the main directory of your project:
cd example-rust
This command generates a generic_handler.rs
file and a main.rs
file in the src
directory. The generic_handler.rs
can be used to
customize a generic event handler. The main.rs
file contains your main
application logic. The Cargo.toml
file contains metadata about your package
and lists its external dependencies.
Example Rust Lambda function code
The following example Rust Lambda function code takes in information about an order, produces a text file receipt, and puts this file in an Amazon S3 bucket.
Example main.rs
Lambda function
use aws_sdk_s3::{Client, primitives::ByteStream};
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::env;
#[derive(Deserialize, Serialize)]
struct Order {
order_id: String,
amount: f64,
item: String,
}
async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
let payload = event.payload;
// Deserialize the incoming event into Order struct
let order: Order = serde_json::from_value(payload)?;
let bucket_name = env::var("RECEIPT_BUCKET")
.map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;
let receipt_content = format!(
"OrderID: {}\nAmount: ${:.2}\nItem: {}",
order.order_id, order.amount, order.item
);
let key = format!("receipts/{}.txt", order.order_id);
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let s3_client = Client::new(&config);
upload_receipt_to_s3(&s3_client, &bucket_name, &key, &receipt_content).await?;
Ok("Success".to_string())
}
async fn upload_receipt_to_s3(
client: &Client,
bucket_name: &str,
key: &str,
content: &str,
) -> Result<(), Error> {
client
.put_object()
.bucket(bucket_name)
.key(key)
.body(ByteStream::from(content.as_bytes().to_vec())) // Fixed conversion
.content_type("text/plain")
.send()
.await?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Error> {
run(service_fn(function_handler)).await
}
This main.rs
file contains the following sections of code:
-
use
statements: Use these to import Rust crates and methods that your Lambda function requires. -
#[derive(Deserialize, Serialize)]
: Define the shape of the expected input event in this Rust struct. -
async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>
: This is the main handler method, which contains your main application logic. -
async fn upload_receipt_to_s3 (...)
: This is a helper method that's referenced by the mainfunction_handler
method. -
#[tokio::main]
: This is a macro that marks the entry point of a Rust program. It also sets up a Tokio runtime, which allows your main()
method to useasync
/await
and run asynchronously. -
async fn main() -> Result<(), Error>
: Themain()
function is the entry point of your code. Within it, we specifyfunction_handler
as the main handler method.
The following Cargo.toml
file accompanies this function.
[package]
name = "example-rust"
version = "0.1.0"
edition = "2024"
[dependencies]
aws-config = "1.5.18"
aws-sdk-s3 = "1.78.0"
lambda_runtime = "0.13.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
For this function to work properly, its
execution role must allow the s3:PutObject
action. Also, ensure that you
define the RECEIPT_BUCKET
environment variable. After a successful invocation,
the Amazon S3 bucket should contain a receipt file.
Valid class definitions for Rust handlers
In most cases, Lambda handler signatures that you define in Rust will have the following format:
async fn function_handler(event: LambdaEvent<T>) -> Result<U, Error>
For this handler:
-
The name of this handler is
function_handler
. -
The singular input to the handler is event, and is of type
LambdaEvent<T>
.-
LambdaEvent
is a wrapper that comes from thelambda_runtime
crate. Using this wrapper gives you access to the context object, which includes Lambda-specific metadata such as the request ID of the invocation. -
T
is the deserialized event type. For example, this can beserde_json::Value
, which allows the handler to take in any generic JSON input. Alternatively, this can be a type likeApiGatewayProxyRequest
if your function expects a specific, pre-defined input type.
-
-
The return type of the handler is
Result<U, Error>
.-
U
is the deserialized output type.U
must implement theserde::Serialize
traint so Lambda can convert the return value to JSON. For example,U
can be a simple type likeString
,serde_json::Value
, or a custom struct as long as it implementsSerialize
. When your code reaches an Ok(U) statement, this indicates successful execution, and your function returns a value of typeU
. -
When your code encounters an error (i.e.
Err(Error)
), your function logs the error in Amazon CloudWatch and returns an error response of typeError
.
-
In our example, the handler signature looks like the following:
async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error>
Other valid handler signatures can feature the following:
-
Omitting the
LambdaEvent
wrapper – If you omitLambdaEvent
, you lose access to the Lambda context object within your function. The following is an example of this type of signature:async fn handler(event: serde_json::Value) -> Result<String, Error>
-
Using the unit type as an input – For Rust, you can use the unit type to represent an empty input. This is commonly used for functions with periodic, scheduled invocations. The following is an example of this type of signature:
async fn handler(): ()) -> Result<Value, Error>
Handler naming conventions
Lambda handlers in Rust don’t have strict naming restrictions. Although you can use any name
for your handler, function names in Rust are generally in snake_case
.
For smaller applications, such as in this example, you can use a single main.rs
file to contain all of your code. For larger projects, main.rs
should contain the
entry point to your function, but you can have additional files for that separate your code into
logical modules. For example, you might have the following file structure:
/example-rust
│── src/
│ ├── main.rs # Entry point
│ ├── handler.rs # Contains main handler
│ ├── services.rs # [Optional] Back-end service calls
│ ├── models.rs # [Optional] Data models
│── Cargo.toml
Defining and accessing the input event object
JSON is the most common and standard input format for Lambda functions. In this example, the function expects an input similar to the following:
{
"order_id": "12345",
"amount": 199.99,
"item": "Wireless Headphones"
}
In Rust, you can define the shape of the expected input event in a struct. In this example,
we define the following struct to represent an Order
:
#[derive(Deserialize, Serialize)]
struct Order {
order_id: String,
amount: f64,
item: String,
}
This struct matches the expected input shape. In this example, the
#[derive(Deserialize, Serialize)]
macro automatically generates code for
serialization and deserialization. This means that we can deserialize the generic input JSON type
into our struct using the serde_json::from_value()
method. This is illustrated in
the first few lines of the handler:
async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
let payload = event.payload;
// Deserialize the incoming event into Order struct
let order: Order = serde_json::from_value(payload)?;
...
}
You can then access the fields of the object. For example, order.order_id
retrieves
the value of order_id
from the original input.
Pre-defined input event types
There are many pre-defined input event types available in the aws_lambda_events
crate. For example, if you intend to invoke your function with API Gateway, including the
following import:
use aws_lambda_events::event::apigw::ApiGatewayProxyRequest;
Then, make sure your main handler uses the following signature:
async fn handler(event: LambdaEvent<ApiGatewayProxyRequest>) -> Result<String, Error> {
let body = event.payload.body.unwrap_or_default();
...
}
Refer to the aws_lambda_events crate
Accessing and using the Lambda context object
The Lambda context object contains information about the
invocation, function, and execution environment. In Rust, the LambdaEvent
wrapper
includes the context object. For example, you can use the context object to retrieve the request ID
of the current invocation with the following code:
async fn function_handler(event: LambdaEvent<Value>) -> Result<String, Error> {
let request_id = event.context.request_id;
...
}
For more information about the context object, see Using the Lambda context object to retrieve Rust function information.
Using the AWS SDK for Rust in your handler
Often, you’ll use Lambda functions to interact with or make updates to other AWS resources. The simplest way to interface with these resources is to use the AWS SDK for Rust.
To add SDK dependencies to your function, add them in your Cargo.toml
file. We
recommend only adding the libraries that you need for your function. In the example code earlier,
we used the aws_sdk_s3::Client
. In the Cargo.toml
file, you can add
this dependency by adding the following line under the [dependencies]
section:
aws-sdk-s3 = "1.78.0"
Note
This may not be the most recent version. Choose the appropriate version for your application.
The, import the dependencies directly in your code:
use aws_sdk_s3::{Client, primitives::ByteStream};
The example code then initializes an Amazon S3 client as follows:
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let s3_client = Client::new(&config);
After you initialize your SDK client, you can then use it to interact with other AWS services.
The example code calls the Amazon S3 PutObject
API in the upload_receipt_to_s3
helper function.
Accessing environment variables
In your handler code, you can reference any environment
variables by using the env::var
method. In this example, we reference the
defined RECEIPT_BUCKET
environment variable using the following line of code:
let bucket_name = env::var("RECEIPT_BUCKET")
.map_err(|_| "RECEIPT_BUCKET environment variable is not set")?;
Using shared state
You can declare shared variables that are independent of your Lambda function's handler code.
These variables can help you load state information during
the Init phase, before your function
receives any events. For example, you can modify the code on this page to use shared state when
initializing the Amazon S3 client by updating the main
function and handler signature:
async fn function_handler(client: &Client, event: LambdaEvent<Value>) -> Result<String, Error> {
...
upload_receipt_to_s3(client, &bucket_name, &key, &receipt_content).await?;
...
}
...
#[tokio::main]
async fn main() -> Result<(), Error> {
let shared_config = aws_config::from_env().load().await;
let client = Client::new(&shared_config);
let shared_client = &client;
lambda_runtime::run(service_fn(move |event: LambdaEvent<Request>| async move {
handler(&shared_client, event).await
}))
.await
Code best practices for Rust Lambda functions
Adhere to the guidelines in the following list to use best coding practices when building your Lambda functions:
-
Separate the Lambda handler from your core logic. This allows you to make a more unit-testable function.
-
Minimize the complexity of your dependencies. Prefer simpler frameworks that load quickly on execution environment startup.
-
Minimize your deployment package size to its runtime necessities. This will reduce the amount of time that it takes for your deployment package to be downloaded and unpacked ahead of invocation.
-
Take advantage of execution environment reuse to improve the performance of your function. Initialize SDK clients and database connections outside of the function handler, and cache static assets locally in the
/tmp
directory. Subsequent invocations processed by the same instance of your function can reuse these resources. This saves cost by reducing function run time.To avoid potential data leaks across invocations, don’t use the execution environment to store user data, events, or other information with security implications. If your function relies on a mutable state that can’t be stored in memory within the handler, consider creating a separate function or separate versions of a function for each user.
-
Use a keep-alive directive to maintain persistent connections. Lambda purges idle connections over time. Attempting to reuse an idle connection when invoking a function will result in a connection error. To maintain your persistent connection, use the keep-alive directive associated with your runtime. For an example, see Reusing Connections with Keep-Alive in Node.js.
-
Use environment variables to pass operational parameters to your function. For example, if you are writing to an Amazon S3 bucket, instead of hard-coding the bucket name you are writing to, configure the bucket name as an environment variable.
-
Avoid using recursive invocations in your Lambda function, where the function invokes itself or initiates a process that may invoke the function again. This could lead to unintended volume of function invocations and escalated costs. If you see an unintended volume of invocations, set the function reserved concurrency to
0
immediately to throttle all invocations to the function, while you update the code. -
Do not use non-documented, non-public APIs in your Lambda function code. For AWS Lambda managed runtimes, Lambda periodically applies security and functional updates to Lambda's internal APIs. These internal API updates may be backwards-incompatible, leading to unintended consequences such as invocation failures if your function has a dependency on these non-public APIs. See the API reference for a list of publicly available APIs.
-
Write idempotent code. Writing idempotent code for your functions ensures that duplicate events are handled the same way. Your code should properly validate events and gracefully handle duplicate events. For more information, see How do I make my Lambda function idempotent?
.