skip.link.title

AWS Lambda Python vs. Node.js performance benchmark

  • You can find the source code for this video in my GitHub Repo.

Intro

More and more companies are migrating to the cloud and adopting serverless technologies. It's not only important to write efficient code but also to choose the right programming language for the job.

Lambda pricing model is based on the amount of memory it consumes as well as the duration of running your code. If you choose the wrong programming language, it can significantly impact the cost of your infrastructure.

Create Python Lambda Function

Let's take a look at the code. We're going to be using official SDKs for each language.

functions/python/function.py
import os
import datetime
import random
import boto3

bucket = os.getenv('BUCKET_NAME')
key = 'thumbnail.png'
table = 'Meta'

s3 = boto3.client('s3')
dynamodb = boto3.resource('dynamodb')


# Download the S3 object and return last modified date
def get_s3_object(bucket, key):
    object = s3.get_object(Bucket=bucket, Key=key)
    object_content = object['Body'].read()
    print(object_content)
    return object['LastModified']


# Save the item to the DynamoDB table
def save(table, date):
    table = dynamodb.Table(table)
    table.put_item(Item={'LastModified': date.isoformat()})


# Lambda handler
def lambda_handler(event, context):
    date = get_s3_object(bucket, key)
    random_number_of_days = random.randint(0, 100000)
    date += datetime.timedelta(days=random_number_of_days)
    save(table, date)

Create Node.js Lambda Function

Next is the Node.js function, which is very similar to the previous one.

functions/nodejs/function.js
const aws = require('aws-sdk');
const s3 = new aws.S3();
const ddb = new aws.DynamoDB({ apiVersion: '2012-08-10' });

// Download the S3 object and print content
const getS3Object = (bucket, key, callback) => {
    const params = {
        Bucket: bucket,
        Key: key
    };

    s3.getObject(params, (err, data) => {
        if (err) return err;
        console.log(data);
        callback(data.LastModified);
    });
};

// Save the item to the DynamoDB table
const save = (table, date, callback) => {
    const modifiedDate = date.toISOString()
    const params = {
        TableName: table,
        Item: { 'LastModified': { S: modifiedDate } }
    };

    ddb.putItem(params, (err, data) => {
        if (err) return err;
        callback(null, 200);
    });
};

// Returns a random number between min and max
const getRandomInt = (min, max) => {
    return Math.random() * (max - min) + min;
};

exports.lambda_handler = (event, context, callback) => {
    const bucket = process.env.BUCKET_NAME;
    const key = 'thumbnail.png';
    const table = 'Meta';

    getS3Object(bucket, key, (date) => {
        const randomNumberOfDays = getRandomInt(0, 100000);
        date.setDate(date.getDate() + randomNumberOfDays);
        save(table, date, callback);
    });
};

Terraform Code to Reproduce

We also have terraform code that you can use to reproduce this example.

terraform/0-provider.tf
provider "aws" {
  region = "us-east-1"
}

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.29.0"
    }
  }

  required_version = "~> 1.0"
}
terraform/1-lambda-bucket.tf
# Generate a random string to create a unique S3 bucket
resource "random_pet" "lambda_bucket_name" {
  prefix = "lambda"
  length = 2
}

# Create an S3 bucket to store lambda source code (zip archives)
resource "aws_s3_bucket" "lambda_bucket" {
  bucket        = random_pet.lambda_bucket_name.id
  force_destroy = true
}

# Disable all public access to the S3 bucket
resource "aws_s3_bucket_public_access_block" "lambda_bucket" {
  bucket = aws_s3_bucket.lambda_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}
terraform/2-images-bucket.tf
# Generate a random string to create a unique S3 bucket
resource "random_pet" "images_bucket_name" {
  prefix = "images"
  length = 2
}

# Create an S3 bucket to store images for benchmark test
resource "aws_s3_bucket" "images_bucket" {
  bucket        = random_pet.images_bucket_name.id
  force_destroy = true
}

# Disable all public access to the S3 bucket
resource "aws_s3_bucket_public_access_block" "images_bucket" {
  bucket = aws_s3_bucket.images_bucket.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Upload test image to S3 bucket
resource "aws_s3_object" "image" {
  bucket = aws_s3_bucket.images_bucket.id

  key    = "thumbnail.png"
  source = "../thumbnail.png"

  etag = filemd5("../thumbnail.png")
}
terraform/3-dynamodb.tf
# Create DynamoDB table
resource "aws_dynamodb_table" "meta" {
  name           = "Meta"
  billing_mode   = "PROVISIONED"
  read_capacity  = 5
  write_capacity = 1000
  hash_key       = "LastModified"

  attribute {
    name = "LastModified"
    type = "S"
  }
}
terraform/4-nodejs-lambda.tf
resource "aws_iam_role" "nodejs_lambda_exec" {
  name = "nodejs-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_policy" "nodejs_s3_bucket_access" {
  name = "NodejsS3BucketAccess"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetObject",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${aws_s3_bucket.images_bucket.id}/*"
      },
    ]
  })
}

resource "aws_iam_policy" "nodejs_dynamodb_access" {
  name = "NodejsDynamoDBAccess"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "dynamodb:GetItem",
          "dynamodb:DeleteItem",
          "dynamodb:PutItem",
          "dynamodb:Scan",
          "dynamodb:Query",
          "dynamodb:UpdateItem",
          "dynamodb:BatchWriteItem",
          "dynamodb:BatchGetItem",
          "dynamodb:DescribeTable",
          "dynamodb:ConditionCheckItem"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:dynamodb:*:*:table/Meta"
      },
    ]
  })
}

resource "aws_iam_role_policy_attachment" "nodejs_lambda_policy" {
  role       = aws_iam_role.nodejs_lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "nodejs_s3_bucket_access" {
  role       = aws_iam_role.nodejs_lambda_exec.name
  policy_arn = aws_iam_policy.nodejs_s3_bucket_access.arn
}

resource "aws_iam_role_policy_attachment" "nodejs_dynamodb_access" {
  role       = aws_iam_role.nodejs_lambda_exec.name
  policy_arn = aws_iam_policy.nodejs_dynamodb_access.arn
}

resource "aws_lambda_function" "nodejs" {
  function_name = "nodejs"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_nodejs.key

  environment {
    variables = {
      BUCKET_NAME = aws_s3_bucket.images_bucket.id
    }
  }

  runtime = "nodejs16.x"
  handler = "function.lambda_handler"

  source_code_hash = data.archive_file.lambda_nodejs.output_base64sha256

  role = aws_iam_role.nodejs_lambda_exec.arn
}

resource "aws_cloudwatch_log_group" "nodejs" {
  name = "/aws/lambda/${aws_lambda_function.nodejs.function_name}"

  retention_in_days = 14
}

data "archive_file" "lambda_nodejs" {
  type = "zip"

  source_dir  = "../${path.module}/functions/nodejs"
  output_path = "../${path.module}/functions/nodejs.zip"
}

resource "aws_s3_object" "lambda_nodejs" {
  bucket = aws_s3_bucket.lambda_bucket.id

  key    = "nodejs.zip"
  source = data.archive_file.lambda_nodejs.output_path

  source_hash = filemd5(data.archive_file.lambda_nodejs.output_path)
}

resource "aws_lambda_function_url" "lambda_nodejs" {
  function_name      = aws_lambda_function.nodejs.function_name
  authorization_type = "NONE"
}

output "nodejs_lambda_url" {
  value = aws_lambda_function_url.lambda_nodejs.function_url
}
terraform/4-python-lambda.tf
resource "aws_iam_role" "python_lambda_exec" {
  name = "python-lambda"

  assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_policy" "python_s3_bucket_access" {
  name = "pythonS3BucketAccess"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetObject",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${aws_s3_bucket.images_bucket.id}/*"
      },
    ]
  })
}

resource "aws_iam_policy" "python_dynamodb_access" {
  name = "pythonDynamoDBAccess"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "dynamodb:GetItem",
          "dynamodb:DeleteItem",
          "dynamodb:PutItem",
          "dynamodb:Scan",
          "dynamodb:Query",
          "dynamodb:UpdateItem",
          "dynamodb:BatchWriteItem",
          "dynamodb:BatchGetItem",
          "dynamodb:DescribeTable",
          "dynamodb:ConditionCheckItem"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:dynamodb:*:*:table/Meta"
      },
    ]
  })
}

resource "aws_iam_role_policy_attachment" "python_lambda_policy" {
  role       = aws_iam_role.python_lambda_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "python_s3_bucket_access" {
  role       = aws_iam_role.python_lambda_exec.name
  policy_arn = aws_iam_policy.python_s3_bucket_access.arn
}

resource "aws_iam_role_policy_attachment" "python_dynamodb_access" {
  role       = aws_iam_role.python_lambda_exec.name
  policy_arn = aws_iam_policy.python_dynamodb_access.arn
}

resource "aws_lambda_function" "python" {
  function_name = "python"

  s3_bucket = aws_s3_bucket.lambda_bucket.id
  s3_key    = aws_s3_object.lambda_python.key

  environment {
    variables = {
      BUCKET_NAME = aws_s3_bucket.images_bucket.id
    }
  }

  runtime = "python3.9"
  handler = "function.lambda_handler"

  source_code_hash = data.archive_file.lambda_python.output_base64sha256

  role = aws_iam_role.python_lambda_exec.arn
}

resource "aws_cloudwatch_log_group" "python" {
  name = "/aws/lambda/${aws_lambda_function.python.function_name}"

  retention_in_days = 14
}

data "archive_file" "lambda_python" {
  type = "zip"

  source_dir  = "../${path.module}/functions/python"
  output_path = "../${path.module}/functions/python.zip"
}

resource "aws_s3_object" "lambda_python" {
  bucket = aws_s3_bucket.lambda_bucket.id

  key    = "python.zip"
  source = data.archive_file.lambda_python.output_path

  source_hash = filemd5(data.archive_file.lambda_python.output_path)
}

resource "aws_lambda_function_url" "lambda_python" {
  function_name      = aws_lambda_function.python.function_name
  authorization_type = "NONE"
}

output "python_lambda_url" {
  value = aws_lambda_function_url.lambda_python.function_url
}

To reproduce this setup, just run terraform init and apply.

terraform init
terraform apply

Performance Benchmark

To run the performance test, we're going to be using the loadtest tool that you can install with npm.

loadtest \
    --concurrency 5 \
    --maxRequests 1000 \
    --rps 10 \
    --keepalive \
    https://<id>.lambda-url.us-east-1.on.aws/

To conclude, Python's function duration time is almost 30% less than Node.js. Since lambda charges you by duration, you can potentially save up to 30% simply by choosing Python language over Node.js to perform exactly the same thing.

If you want to find out the results of the benchmark test between Golang and Node.js, you can watch this video.

top.title