Lambda Performance Comparison Golang vs Java


Introduction

AWS Lambda enables developers to focus on solving customer problems by abstracting server management and scaling, with a pay-as-you-go pricing model that makes it ideal for testing new ideas.

Choosing the right language and runtime for Lambda is critical, as it affects cold start times, execution speed, and memory usage. There’s no one-size-fits-all solution; the choice depends on trade-offs aligned with the specific use case.

In this blog post, we’ll compare Golang and Java for building AWS Lambda functions.

Key Performance Metrics

We will mainly focus on the following key metrics in evaluating lambda performance.

Cold Start Time

Whenever the function is invoked for an execution, AWS has to provision an execution environment configured with the runtime which is declared by user, with all the configurations.

All of this provisioning is called as INIT phase of the lambda function.

The duration of the INIT phase is commonly referred to as the cold start time for the function.

Execution Time

Total execution duration of function. This infamously also includes the time in which your runtime is getting initialised which can be slow for certain languages.

Memory Allocation

The amount of memory which will be used by the function during the invocation.

Test Bench

We will use the following infrastructure for benchmarking the performance of Lambda. I have kept the code as close as possible to typical day-to-day examples.

Lambda Benchmark Test Bench
Benchmarking Note #1

INFO: Java functions have increased execution timeout of 60 seconds, as 3 seconds was not working on test bench. To have parity Golang functions were executed once with 3 seconds timeout and 60 seconds timeout.

Benchmarking Note #2

INFO: All the lambda functions will be executed on ARM architecture.

Performance Comparison: Golang vs Java

I used the following script to generate load on the API endpoint. It is using k6 to gradually rampup up the connections and number of virtual users to the endpoint.

import http from "k6/http";
import { sleep } from "k6";

export const options = {
    stages: [
        { duration: "30s", target: 20 },
        { duration: "1m30s", target: 10 },
        { duration: "20s", target: 0 },
    ],
};

export default function () {
    // Making a call to endpoint
    http.get(__ENV.API_ENDPOINT);

    // Sleeping for 1 second to simulate real world traffic
    sleep(1);
}
Golang code
package main

import (
	"bytes"
	"context"
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"os"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/kms"
	"github.com/aws/aws-sdk-go-v2/service/s3"
)

var (
	s3Client  *s3.Client
	kmsClient *kms.Client
)

func init() {
	// Initialize the S3 client outside of the handler, during the init phase
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		log.Fatalf("unable to load SDK config, %v", err)
	}

	s3Client = s3.NewFromConfig(cfg)
	kmsClient = kms.NewFromConfig(cfg)
}

func uploadReceiptToS3(ctx context.Context, bucketName string, key string, content []byte) error {
	_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
		Bucket: &bucketName,
		Key:    &key,
		Body:   bytes.NewReader(content),
	})
	if err != nil {
		log.Printf("Failed to upload receipt to S3: %v", err)
		return err
	}
	return nil
}

func getRandomId() (string, error) {
	randBytes := make([]byte, 16)
	_, err := rand.Read(randBytes)
	if err != nil {
		log.Printf("Error while creating random bits %v\n", err)
		return "", err
	}

	randomId := hex.EncodeToString(randBytes)
	return randomId, nil
}

func getFile(ctx context.Context, bucketName string, key string) ([]byte, error) {
	result, err := s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: &bucketName,
		Key:    &key,
	})
	if err != nil {
		log.Printf("Received error while get object %v", err)
		return nil, err
	}
	defer result.Body.Close()
	body, err := io.ReadAll(result.Body)
	if err != nil {
		log.Printf("Error while reading bytes from output %v\n", err)
		return nil, err
	}

	return body, nil
}

func encryptContent(ctx context.Context, keyId string, content []byte) ([]byte, error) {
	encOut, err := kmsClient.Encrypt(ctx, &kms.EncryptInput{
		KeyId:     &keyId,
		Plaintext: content,
	})
	if err != nil {
		log.Printf("Unable to encrypt the object from kms %v\n", err)
		return nil, err
	}

	return encOut.CiphertextBlob, nil
}

type Response struct {
	Body string `json:"body"`
}

func handleRequest(ctx context.Context) (*Response, error) {
	fileLocationBucket := os.Getenv("FILE_LOCATION_BUCKET")
	if fileLocationBucket == "" {
		log.Printf("FILE_LOCATION_BUCKET environment variable is not set")
		return &Response{}, fmt.Errorf(
			"missing required environment variable FILE_LOCATION_BUCKET",
		)
	}

	fileName := os.Getenv("FILE_NAME")
	if fileName == "" {
		log.Printf("FILE_NAME environment variable is not set")
		return &Response{}, fmt.Errorf("missing required environment variable FILE_NAME")
	}

	keyId := os.Getenv("KEY_ID")
	if keyId == "" {
		log.Printf("KEY_ID environment variable is not set")
		return &Response{}, fmt.Errorf("missing required environment variable KEY_ID")
	}

	uploadBucket := os.Getenv("UPLOAD_BUCKET")
	if uploadBucket == "" {
		log.Printf("UPLOAD_BUCKET environment variable is not set")
		return &Response{}, fmt.Errorf("missing required environment variable UPLOAD_BUCKET")
	}

	fileData, err := getFile(ctx, fileLocationBucket, fileName)
	if err != nil {
		return &Response{}, fmt.Errorf("Unable to fetch file from S3")
	}

	encText, err := encryptContent(ctx, keyId, fileData)
	if err != nil {
		return &Response{}, fmt.Errorf("Unable to encrypt the content using KMS")
	}

	randomId, err := getRandomId()
	if err != nil {
		return &Response{}, fmt.Errorf("Unable to generate the random id")
	}

	err = uploadReceiptToS3(
		ctx,
		uploadBucket,
		randomId,
		encText,
	)
	if err != nil {
		return &Response{}, fmt.Errorf("Unable to upload the encrypted file to S3")
	}

	response := Response{Body: randomId}
	log.Printf("Response returned %v\n", response)
	return &response, nil
}

func main() {
	lambda.Start(handleRequest)
}
Java Code
package org.example;

import java.io.IOException;
import java.security.SecureRandom;

import com.amazonaws.services.lambda.runtime.Context;

import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.kms.KmsClient;
import software.amazon.awssdk.services.kms.model.EncryptRequest;
import software.amazon.awssdk.services.kms.model.EncryptResponse;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

public class App {
    private static S3Client s3Client;
    private static KmsClient kmsClient;
    private static SdkHttpClient httpClient = UrlConnectionHttpClient.create();
    private static Region deployRegion = Region.AP_SOUTH_1;

    static {
        s3Client = S3Client.builder()
                .region(deployRegion)
                .httpClient(httpClient)
                .build();

        kmsClient = KmsClient.builder()
                .region(deployRegion)
                .httpClient(httpClient)
                .build();
    }

    private static byte[] fetchFileFromS3(String fileLocationBucket, String fileName) throws IOException {
        ResponseInputStream<GetObjectResponse> respObject = s3Client.getObject(GetObjectRequest.builder()
                .bucket(fileLocationBucket)
                .key(fileName)
                .build());
        return respObject.readAllBytes();
    }

    private static byte[] encryptFile(String keyId, byte[] content) {
        EncryptResponse response = kmsClient.encrypt(EncryptRequest.builder()
                .keyId(keyId)
                .plaintext(SdkBytes.fromByteArray(content))
                .build());
        return response.ciphertextBlob().asByteArray();
    }

    private static String getRandomId() {
        // Generate 16 random bytes
        byte[] randBytes = new byte[16];
        SecureRandom secureRandom = new SecureRandom();
        secureRandom.nextBytes(randBytes);

        // Convert to hex string
        StringBuilder hexString = new StringBuilder();
        for (byte b : randBytes) {
            hexString.append(String.format("%02x", b));
        }

        return hexString.toString();
    }

    private static void uploadEncryptedFile(String uploadBucket, String fileId, byte[] content) {
        s3Client.putObject(PutObjectRequest.builder()
                .bucket(uploadBucket)
                .key(fileId)
                .build(), RequestBody.fromBytes(content));
    }

    public String handleRequest(Context context) {
        String fileLocationBucket = System.getenv("FILE_LOCATION_BUCKET");
        String fileName = System.getenv("FILE_NAME");
        String keyId = System.getenv("KEY_ID");
        String uploadBucket = System.getenv("UPLOAD_BUCKET");

        try {
            // Fetch the file from s3 bucket.
            byte[] fileByte = fetchFileFromS3(fileLocationBucket, fileName);
            // Encrypt the file from keyId
            byte[] encryptedFile = encryptFile(keyId, fileByte);
            // Random id for every upload
            String randomId = getRandomId();
            // Upload the encrypted file to upload bucket.
            uploadEncryptedFile(uploadBucket, randomId, encryptedFile);
            return randomId;
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Failure while executing lambda");
        }
    }
}

Cold Start Time Values

TypeInvocationsCold Start CountAverage Init Duration (in ms)Minimum Init Duration (in ms)Maximum Init Duration (in ms)
Golang (128 MB, 3 second timeout)16214141.035136.61145.25
Golang (256 MB, 3 second timeout)16145141.57138.41145.36
Golang (512 MB, 3 second timeout)16019142.089137.27149.58
Golang (1024 MB, 3 second timeout)16336140.905137.27149.62
Golang (2048 MB, 3 second timeout)14948134.963128.73140.55
Golang (128 MB, 60 second timeout)155312140.097134.12146.12
Golang (256 MB, 60 second timeout)16465140.308136.87144.53
Golang (512 MB, 60 second timeout)16445140.33134.86144.32
Golang (1024 MB, 60 second timeout)16374138.42133.78147.45
Golang (2048 MB, 60 second timeout)16334139.71134.78146.33
Java (128 MB, 60 second timeout)1278111650.811612.641687.69
Java (256 MB, 60 second timeout)149371712.041588.021794.87
Java (512 MB, 60 second timeout)157671707.031601.661770.89
Java (1024 MB, 60 second timeout)161051735.721703.041766.08
Java (2048 MB, 60 second timeout)149691504.651453.951556.83

Insights around Cold Start Times

  1. Golang has a significantly lower and more stable cold start times (regardless of memory or timeout) as compared to Java.
  2. Java has much higher cold start times, averaging over 1500 ms, and these times increase with memory, though they slightly decrease at higher memory settings.
  3. Memory does not affect Golang cold starts much, but in Java, cold start times are noticeably impacted by memory size.
  4. Function timeout does not have any impact on the cold start times.

Maximum Memory Consumption

TypeInvocationsProvisioned Memory MBSmallest Memory Request MBAvg Memory Used MBMax Memory Used MBOver Provisioned MB
Golang (128 MB, 3 second timeout)1621122.0741.00843.313643.86978.2013
Golang (256 MB, 3 second timeout)1614244.14141.00842.981543.869200.272
Golang (512 MB, 3 second timeout)1601488.28144.822750.108752.4521435.829
Golang (1024 MB, 3 second timeout)1633976.56244.822749.811852.4521924.11
Golang (2048 MB, 3 second timeout)14941953.1243.86949.606451.49841901.63
Golang (128 MB, 60 second timeout)1553122.0741.00842.741643.86978.2013
Golang (256 MB, 60 second timeout)1646244.14141.00843.156443.869200.272
Golang (512 MB, 60 second timeout)1644488.28143.86949.632852.4521435.829
Golang (1024 MB, 60 second timeout)1637976.56243.86949.972152.4521924.11
Golang (2048 MB, 60 second timeout)16331953.1244.822751.17653.40581899.72
Java (128 MB, 60 second timeout)1278122.07114.441116.374118.2563.8147
Java (256 MB, 60 second timeout)1493244.141159.264164.164166.89377.2476
Java (512 MB, 60 second timeout)1576488.281158.31168.201173.569314.712
Java (1024 MB, 60 second timeout)1610976.562161.171170.339175.476801.086
Java (2048 MB, 60 second timeout)14961953.12169.754176.334182.1521770.97

Insights around Memory Consumption

  1. Java has much higher memory usage, with a noticeable increase in memory usage as memory provisioned increases. This is expected as there is JVM’s inherent overhead.
  2. Golang uses significantly less memory than it is allocated. It more or less uses the same amount of memory across varied memory settings.
  3. Java average memory usage increases linearly as the amount of memory provisioned increases.

Billed Duration

TypeInvocationsMax Billd Duration msMin Billed Duration msAvg Billed Duration msp99 Billed Duration msp95 Billed Duration msp50 Billed Duration ms
Golang (128 MB, 3 second timeout)162117833864.8723195.85295.939654.9815
Golang (256 MB, 3 second timeout)16148983858.15241229148
Golang (512 MB, 3 second timeout)16015533761.2573131.968101.97150.9597
Golang (1024 MB, 3 second timeout)16333713959.44031249851
Golang (2048 MB, 3 second timeout)14944253757.71421249449
Golang (128 MB, 60 second timeout)155316813671.9581271.83695.939651.9886
Golang (256 MB, 60 second timeout)16468403855.7911178647
Golang (512 MB, 60 second timeout)16444823554.62961108448
Golang (1024 MB, 60 second timeout)16373793757.1539122.92795.939647.9935
Golang (2048 MB, 60 second timeout)16333463757.40911189249
Java (128 MB, 60 second timeout)127814418123363.7931360.21466.812201.814
Java (256 MB, 60 second timeout)1493664249134.787489.754202.82585.9507
Java (512 MB, 60 second timeout)157631374687.4061273.744122.92764.9692
Java (1024 MB, 60 second timeout)161015494569.7478143.957101.97157.9726
Java (2048 MB, 60 second timeout)14968284671.607149.979104.9760.9434

Insights around Billed Duration

  1. Golang billed durations are much lower as compared to Java’s across all the memory configurations.
  2. Memory scaling affects Java’s performance more significantly than Golang’s. Java’s billed durations decrease with higher memory allocations.
  3. Max billed duration in Java is very high compared to Golang, which is expected due to overhead of JVM initialization, garbage collection, and other internal processes.

Cost

ConfigurationInvocationsAvg Billed Duration (ms)Memory (MB)Cost for Requests (USD)Cost for Duration (USD)Total Cost (USD)
Golang (128 MB, 3 second timeout)162164.871280.00032420.0001370.0004612
Golang (256 MB, 3 second timeout)161458.152560.00032280.0001470.0004698
Golang (512 MB, 3 second timeout)160161.265120.00032020.0002140.0005342
Golang (1024 MB, 3 second timeout)163359.4410240.00032660.0003410.0006676
Golang (2048 MB, 3 second timeout)149457.7120480.00029880.000520.0008188
Golang (128 MB, 60 second timeout)155371.961280.00031060.0001470.0004576
Golang (256 MB, 60 second timeout)164655.792560.00032920.0001470.0004762
Golang (512 MB, 60 second timeout)164454.635120.00032880.0002130.0005418
Golang (1024 MB, 60 second timeout)163757.1510240.00032740.0003390.0006664
Golang (2048 MB, 60 second timeout)163357.4120480.00032660.0005180.0008446
Java (128 MB, 60 second timeout)1278363.791280.00025560.0019360.0021916
Java (256 MB, 60 second timeout)1493134.792560.00029860.0006870.0009865
Java (512 MB, 60 second timeout)157687.415120.00031520.0007660.0010812
Java (1024 MB, 60 second timeout)161069.7510240.0003220.0011420.0014642
Java (2048 MB, 60 second timeout)149671.6120480.00029920.0015320.0018312

Insights around Cost

  1. Golang incurs much lower cost as compared to Java, while maintaining greater throughput as compared to Java.
  2. Java cost decreases with increasing memory, which is expected as more resources are available to JVM for optimal usage and performance.

Conclusion

Benchmarking shows that Golang offers a better performance-to-cost ratio than Java for AWS Lambda functions, with faster execution, lower memory usage, and more consistent performance.

However, the JVM ecosystem has introduced improvements like GraalVM’s AOT Compilation and SnapStart, which address Java’s cold start issues. I plan to explore these advancements further to evaluate their impact on Java’s performance and competitiveness with Golang in serverless environments.