Skip to main content

Serverless Speed: Rust vs. Go, Java, and Python in AWS Lambda Functions

Serverless Speed: Rust vs. Go, Java, and Python in AWS Lambda Functions

Takeaways

  • Use 1.5GB+ memory allocation for best S3 thruput
  • Benchmark JSON libraries

Rust Lambda setup

// main.rs
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
use rusoto_core::Region;
use rusoto_s3::{S3Client, S3};
use serde::{Deserialize, Serialize};
use tokio::io::{AsyncReadExt, AsyncBufReadExt};

#[derive(Deserialize)]
struct Request {
    bucket: String,
    key: String,
}

#[derive(Serialize)]
struct Response {
    req_id: String,
    msg: String,
}

async fn handle_request(event: LambdaEvent<Request>) -> Result<Response, Error> {
    let started_at = std::time::Instant::now();
    let client = S3Client::new(Region::UsWest2);

    let output = client
        .get_object(rusoto_s3::GetObjectRequest {
            bucket: bucket.to_string(),
            key: key.to_string(),
            ..Default::default()
        })
        .await?;

    let Some(body) = output.body else {
        return Err(anyhow::anyhow!("No body found in S3 response").into());
    };

    let body = body.into_async_read();
    let body = tokio::io::BufReader::new(body);
    let decoder = async_compression::tokio::bufread::ZstdDecoder::new(body);
    let reader = tokio::io::BufReader::new(decoder);

    let mut lines = reader.lines();
    let mut num_log_events = 0;
    while let Ok(Some(mut line)) = lines.next_line().await {
        let _value = unsafe {
            simd_json::to_borrowed_value(line.as_mut_str().as_bytes_mut())?
        };
        num_log_events += 1;
        if num_log_events % 1000 == 0 {
            println!("num_log_events={}", num_log_events);
        }
    }

    let msg = format!(
        "elapsed={:?} num_log_events={}",
        started_at.elapsed(),
        num_log_events
    );
    Ok(Response {
        req_id: event.context.request_id,
        msg,
    })
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        .init();

    run(service_fn(handle_request)).await
}

Build:

# build.sh
cargo lambda build --release --arm64
(cd ./target/lambda/lambda_langs_test_rust/ && zip ./bootstrap.zip ./bootstrap)

Deploy:

aws lambda create-function \
--function-name lambda_langs_test_rust \
--runtime provided.al2 \
--memory-size 640  \
--architectures arm64 \
--zip-file ./bootstrap.zip \
--handler unused \
--timeout 900 \
--role ${LAMBDA_IAM_ROLE}

Run:

aws lambda invoke \
  --function-name lambda_langs_test_python \
  --log-type Tail \
  --cli-binary-format raw-in-base64-out \
  --payload '{"bucket": "<s3_bucket>", "key": "<s3_key>"}' \
  ./response.json \
| jq -r .LogResult | base64 --decode