What will We explore and learn in this article?
In this article, we will explore some ways to develop, deploy and run applications on AWS Lambda using Quarkus framework. Of course, we will measure performance (the cold and warm start times) of the Lambda function. We will also show how we can optimise performance of Lambda functions using Lambda SnapStart (including various priming techniques) and GraalVM Native Image deployed as AWS Lambda Custom Runtime. You can find code examples for the whole series in my GitHub Account.
Table of Contents
- What will We explore and learn in this article?
- Example application with the Quarkus framework on AWS Lambda
- Measurements of cold and warm start times of our application
- 1. Sample application as presented above
- 2. Sample application with the activated AWS Lambda SnapStart without using priming
- 3. Sample application with the activated AWS Lambda SnapStart with application of priming for DynamoDB request
- 4. Sample application with the activated AWS Lambda SnapStart with application of priming of API Gateway Request Event
- 5. a rebuilt sample application that we build GraalVM Native Image and deploy it as Lambda Custom Runtime
- Performance measurement results
- Conclusion
Example application with the Quarkus framework on AWS Lambda
To explain this, we will use a simple example application, the architecture of which is shown below.

In this application, we will create products and retrieve them by their ID and use Amazon DynamoDB as a NoSQL database for the persistence layer. We use Amazon API Gateway which makes it easy for developers to create, publish, maintain, monitor and secure APIs and AWS Lambda to execute code without the need to provision or manage servers. We also use AWS SAM, which provides a short syntax optimised for defining infrastructure as code (hereafter IaC) for serverless applications. For this article, I assume a basic understanding of the mentioned AWS services, serverless architectures in AWS, Quarkus framework and GraalVM including its Native Image capabilities.
In order to build and deploy the sample application, we need the following local installations: Java 21, Maven, AWS CLI and SAM CLI. For the GraalVM example, we also need GraalVM and Native Image. For the GraalVM example, additionally GraalVM and Native Image.
Now let’s look at relevant source code fragments and start with the sample application that we will run directly on the managed Java 21 runtime of AWS Lambda. AWS Lambda only supports managed Java LTS versions, so version 21 is currently the latest.
First, let’s take a look at the source code of the GetProductByIdHandler Lambda function. This Lambda function determines the product based on its ID and returns it.
@Named("getProductById")
public class GetProductByIdHandler implements RequestHandler {
@Inject
private ObjectMapper objectMapper;
@Inject
private DynamoProductDao productDao;
@Override
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) {
String id = requestEvent.getPathParameters().get("id");
Optional<Product> optionalProduct = productDao.getProduct(id);
try {
if (optionalProduct.isEmpty()) {
context.getLogger().log(" product with id " + id + " not found ");
return new APIGatewayProxyResponseEvent().withStatusCode(HttpStatusCode.NOT_FOUND)
.withBody("Product with id = " + id + " not found");
}
context.getLogger().log(" product " + optionalProduct.get() + " found ");
return new APIGatewayProxyResponseEvent().withStatusCode(HttpStatusCode.OK)
.withBody(objectMapper.writeValueAsString(optionalProduct.get()));
} catch (Exception je) {
je.printStackTrace();
return new APIGatewayProxyResponseEvent().withStatusCode(HttpStatusCode.INTERNAL_SERVER_ERROR)
.withBody("Internal Server Error :: " + je.getMessage());
}
}
}
We annotate the Lambda function with @Named(“getProductById”), which will be important for mapping, and inject the implementation of DynamoProductDao. The method handleRequest receives an object of type APIGatewayProxyRequestEvent as input, as APIGatewayRequest invokes the Lambda function, from which we retrieve the product ID by invoking requestEvent.getPathParameters().get(“id”) and ask our DynamoProductDao to find the product with this ID in the DynamoDB by calling productDao.getProduct(id). Depending on whether the product exists or not, we wrap the Jackson serialised response in an object of type APIGatewayProxyResponseEvent and send it back to Amazon API Gateway as a response. The source code of the Lambda function CreateProductHandler, which we use to create and persist products, looks similar.
The source code of the Product entity looks very simple:
public record Product(String id, String name, BigDecimal price) {}
The implementation of the DynamoProductDao persistence layer uses AWS SDK for Java 2.0 to write to or read from the DynamoDB. Here is an example of the source code of the getProductById method, which we used in the GetProductByIdHandler Lambda function described above:
public Optional<Product> getProduct(String id) {
GetItemResponse getItemResponse= dynamoDbClient.getItem(GetItemRequest.builder()
.key(Map.of("PK", AttributeValue.builder().s(id).build()))
.tableName(PRODUCT_TABLE_NAME)
.build());
if (getItemResponse.hasItem()) {
return Optional.of(ProductMapper.productFromDynamoDB(getItemResponse.item()));
} else {
return Optional.empty();
}
}
Here we use the instance of DynamoDbClient Client to build GetItemRequest to query DynamoDB table, whose name we get from environment variable (which we will set in AWS SAM template) by invoking System.getenv(“PRODUCT_TABLE_NAME”), for the product based on its ID. If the product is found, we use the custom written ProductMapper to map the DynamoDB item to the attributes of the product entity.
Apart from annotation, we have not yet seen any dependencies on the Quarkus framework. We can see how everything interacts in pom.xml. Apart from dependencies to the Quarkus framework (we are using version 3.18.3, but you are welcome to upgrade to the newer version and most of it should work the same), AWS SDK for Java and other AWS artefacts, we see the following dependency,
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-amazon-lambda</artifactId>
</dependency>
which is a bridge between AWS Lambda and Quarkus Framework. Now let’s look at the last missing part, namely IaC with AWS SAM, which is defined in template.yaml. There we declare Amazon API Gateway (incl. UsagePlan and API Key), AWS Lambdas and DynamoDB table. We first look at the definition of the lambda function GetProductByIdFunction:
GetProductByIdFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: GetProductByIdWithWithQuarkus318
AutoPublishAlias: liveVersion
Policies:
- DynamoDBReadPolicy:
TableName: !Ref ProductsTable
Environment:
Variables:
QUARKUS_LAMBDA_HANDLER: getProductById
Events:
GetRequestById:
Type: Api
Properties:
RestApiId: !Ref MyApi
Path: /products/{id}
Method: get
We see that this Lambda function is linked to the HTTP Get call method and the path /products/{id} of the API gateway. But how is GetProductByIdHandler Lambda implementation resolved? We see the environment variable QUARKUS_LAMBDA_HANDLER, whose value getProductById matches the value of the named annotation (@Named(“getProductById”)) on the GetProductByIdHandler class. The resolution itself is performed by the generic io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler Lambda handler, which is defined in template.yaml in the Globals section of the Lambda functions as follows:
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
CodeUri: target/function.zip
Runtime: java21
....
Environment:
Variables:
...
PRODUCT_TABLE_NAME: !Ref ProductsTable
....
Other parameters are also defined there that are valid for all defined Lambda functions, such as Java runtime environment Java 21 and CodeURI. We have also set DynamoDB table name as environment variable, which is used in the DynamoProductDao class.
Now we have to build the application with mvn clean package (function.zip is created and stored in the subdirectory named target) and deploy it with sam deploy -g. We will see our customised Amazon API Gateway URL in the return. We can use it to create products and retrieve them by ID. The interface is secured with the API key. We have to send the following as HTTP header: “X-API-Key: a6ZbcDefQW12BN56WEV318”, see MyApiKey definition in template.yaml. To create the product with ID=1, we can use the following curl query:
curl -m PUT -d '{ "id": 1, "name": "Print 10x13", "price": 0.15 }' -H "X-API-Key: a6ZbcDefQW12BN56WEV318" https://{$API_GATEWAY_URL}/prod/products
For example, to query the existing product with ID=1, we can use the following curl query:
curl -H "X-API-Key: a6ZbcDefQW12BN56WEV318" https://{$API_GATEWAY_URL}/prod/products/1
In both cases, we need to replace the {$API_GATEWAY_URL} with the individual Amazon API Gateway URL that is returned by the sam deploy -g command. We can also search for this URL when navigating to our API in the Amazon API Gateway service in the AWS console.
Measurements of cold and warm start times of our application
In the following, we will measure the performance of our GetProductByIdFunction Lambda function, which we will trigger by invoking curl -H ‘X-API-Key: a6ZbcDefQW12BN56WEV318’ https://{$API_GATEWAY_URL}/prod/products/1. Two aspects are important to us in terms of performance: cold and warm start times. It is known that Java applications in particular have a very high cold start time. The article Understanding the Lambda execution environment lifecycle provides a good overview of this topic. We will also present various performance optimization options so that we can make 5 different measurements:
- Sample application as we have presented it above
- Sample application with the activated AWS Lambda SnapStart without using priming
- Sample application with the activated AWS Lambda SnapStart with application of priming from DynamoDB request
- Sample application with the activated AWS Lambda SnapStart with priming of the API Gateway request event
- A rebuilt sample application that we build GraalVM Native Image and deploy it as Lambda Custom Runtime
Now we will go through all 5 performance measurements methods and only at the end will we show how we carried out performance measurements and summarise the results for all 5 methods.
1. Sample application as presented above
To perform the performance measurement, we must ensure that AWS Lambda SnapStart, which we will introduce in a moment, is deactivated with #. This is done in template.yaml as follows:
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
CodeUri: target/function.zip
Runtime: java21
#SnapStart:
#ApplyOn: PublishedVersions
....
It is therefore necessary that we perform the 4 measurements on one and the same application and therefore we have to switch some things on and off for each measurement. It is conceivable to solve it more elegantly and e.g. define several AWS SAM templates, with and without SnapStart.
2. Sample application with the activated AWS Lambda SnapStart without using priming
As we will see, without any optimizations, the performance measurements for method 1 will show quite high values, especially for the cold start times. We will therefore present various optimization options for methods 2-5. Lambda SnapStart is one of them.
Lambda SnapStart can provide a start time of a lambda function of less than one second. SnapStart simplifies the development of responsive and scalable applications without provisioning resources or implementing complex performance optimizations.
The largest portion of startup latency (often referred to as cold start time) is the time Lambda spends initializing the function, which includes loading the function code, starting the runtime and initialising the function code. With SnapStart, Lambda initializes our function when we publish a function version. Lambda takes a Firecracker microVM snapshot of the memory and disk state of the initialised execution environment, encrypts the snapshot and intelligently caches it to optimize retrieval latency.
To ensure reliability, Lambda manages multiple copies of each snapshot. Lambda automatically patches snapshots and their copies with the latest runtime and security updates. When we call the function version for the first time and as the calls increase, Lambda continues new execution environments from the cached snapshot instead of initialising them from scratch, which improves startup latency. More information can be found in the article Reducing Java cold starts on AWS Lambda functions with SnapStart. I have published the whole series about Lambda SnapStart for Java applications online.
To activate Lambda SnapStart, we have to do the following in template.yaml for the Lambda function:
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
CodeUri: target/function.zip
Runtime: java21
SnapStart:
ApplyOn: PublishedVersions
....
This can be done in the globals section of the Lambda functions, in which case SnapStart applies to all Lambda functions defined in the AWS SAM template, or you can add the 2 lines
SnapStart:
ApplyOn: PublishedVersions
to activate SnapStart only for the individual Lambda function. To perform the performance measurement without priming techniques, as we will introduce in methods 3 and 4, please either comment out or remove @Startup annotation in the following 2 Java classes AmazonDynamoDBPrimingResource and AmazonAPIGatewayPrimingResource .
3. Sample application with the activated AWS Lambda SnapStart with application of priming for DynamoDB request
We have already learnt about Lambda SnapStart in method 2. Activating Lambda SnapStart is also a prerequisite for this method. As we will see later, cold start times are significantly reduced simply by activating SnapStart without changing our source code. However, there are other techniques to reduce this, which we call priming and which require changes to the source code.
SnapStart and runtime hooks offer you new possibilities to create your Lambda functions for low startup latency. With the pre snapshot hook, we can prepare our Java application as much as possible for the first call. We load and initialize as much as possible which our Lambda function needs before the snapshot is created. This technique is known as priming.
In this method I will introduce you to the priming of DynamoDB request, which is implemented in the AmazonDynamoDBPrimingResource class.
@Startup
@ApplicationScoped
public class AmazonDynamoDBPrimingResource implements Resource {
@Inject
private ObjectMapper objectMapper;
@Inject
private DynamoProductDao productDao;
@PostConstruct
public void init () {
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
productDao.getProduct("0");
}
@Override
public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
}
}
We use CRaC runtime hooks here. To do this, we need to declare the following dependency in pom.xml:
<dependency>
<groupId>io.github.crac</groupId>
<artifactId>org-crac</artifactId>
</dependency>
AmazonDynamoDBPrimingResource class is annotated with @Startup annotation (so that this class/bean is initialized directly when the application is started) and implements org.crac.Resource interface. The class registers itself as a CRaC resource in the init method annotated with @PostConstruct annotation. The priming itself happens in the method where we ask DynamoDB for the product with the ID equal to 0. beforeCheckpoint method is CRaC runtime hook that is invoked before creating the microVM snapshot. We are not even interested in the result of the call productDao.getProduct(“0”), but with this call all required classes are instantiated and the expensive one-time initialisation of HTTP Client (default is Apache HTTP Client ) and Jackson Marshallers (for the purpose of converting Java objects to JSON and vice versa) is carried out. As this is done during the deployment phase of the Lambda function when SnapStart is activated and before the snapshot is created, the snapshot will already contain all of this. After the fast snapshot restore phase during the Lambda invoke, we will gain a lot in performance in case of cold start by priming this way (see measurements below). We therefore prime the DynamoDB request.
To ensure that only this priming takes effect, please either comment out or remove the @Startup annotation in the following AmazonAPIGatewayPrimingResou class.
4. Sample application with the activated AWS Lambda SnapStart with application of priming of API Gateway Request Event
Here I’ll present you another experimental priming technique that preinitializes the entire web request (API gateway request event). This preinitializes more than method 3, but also requires significantly more code to be written. The idea is nevertheless comparable. Activating Lambda SnapStart is also a prerequisite for this method. Let’s take a look at the implementation in the AmazonAPIGatewayPrimingResource class:
@Startup
@ApplicationScoped
public class AmazonAPIGatewayPrimingResource implements Resource {
@PostConstruct
public void init () {
Core.getGlobalContext().register(this);
}
@Override
public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
new QuarkusStreamHandler().handleRequest
(new ByteArrayInputStream(convertAwsProxRequestToJsonBytes()),
new ByteArrayOutputStream(), new MockLambdaContext());
}
@Override
public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
}
private static byte[] convertAwsProxRequestToJsonBytes () throws JsonProcessingException {
ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter();
return ow.writeValueAsBytes(getAwsProxyRequest());
}
private static AwsProxyRequest getAwsProxyRequest () {
final AwsProxyRequest awsProxyRequest = new AwsProxyRequest ();
awsProxyRequest.setHttpMethod("GET");
awsProxyRequest.setPath("/products/0");
awsProxyRequest.setResource("/products/{id}");
awsProxyRequest.setPathParameters(Map.of("id","0"));
final AwsProxyRequestContext awsProxyRequestContext = new AwsProxyRequestContext();
final ApiGatewayRequestIdentity apiGatewayRequestIdentity= new ApiGatewayRequestIdentity();
apiGatewayRequestIdentity.setApiKey("blabla");
awsProxyRequestContext.setIdentity(apiGatewayRequestIdentity);
awsProxyRequest.setRequestContext(awsProxyRequestContext);
return awsProxyRequest;
}
}
Please make sure that @Startup annotation is present in the AmazonAPIGatewayPrimingResource class so that the priming takes effect. As we can see, in the method getAwsProxyRequest we create object of type AwsProxyRequest, which is sent to the path /products/{id} and sets ID =0. In the CRaC runtime hook beforeCheckpoint method, AwsProxyRequest is converted into a byte array and processed by calling QuarkusStreamHandler().handleRequest. This basically mocks APIGatewayProxyRequestEvent and the call is mapped to the Lambda function GetProductByIdHandler, which handleRequest method is called directly. The priming is performed locally in AWS, so no network trip is required.
The purpose of this priming is to instantiate all required classes and to translate the AWS Lambda programming model (and invocation) into the Quarkus programming model. Through the preinitialized call of the handleRequest method of the GetProductByIdHandler, the priming presented in method number 3 is also carried out automatically by the DynamoDB call.
To ensure that only this priming takes effect, please either comment out or remove the @Startup annotation in the following class AmazonDynamoDBPrimingResource.
However, I consider this priming technique to be experimental, as it naturally leads to a lot of extra code, which can be significantly simplified using a few utility methods. Therefore, the decision to use this priming method is left to the reader.
5. a rebuilt sample application that we build GraalVM Native Image and deploy it as Lambda Custom Runtime
This article assumes prior knowledge of GraalVM and its native image capabilities. For a concise overview about them and how to get both installed, please refer to the following articles: Introduction to GraalVM, GraalVM Architecture and GraalVM Native Image.
Let’s take a look at the differences to our previous example application. As far as the source code of the application is concerned, the ReflectionConfig class has been added. In this class, we use @RegisterForReflection annotation to define the classes that are only loaded at runtime.
@RegisterForReflection(targets = {
APIGatewayProxyRequestEvent.class,
HashSet.class,
APIGatewayProxyRequestEvent.ProxyRequestContext.class,
APIGatewayProxyRequestEvent.RequestIdentity.class,
DateTime.class,
Product.class,
Products.class,
})
Since GraalVM uses Native Image Ahead-of-Time compilation, we need to provide such classes in advance, otherwise ClassNotFound errors will be thrown at runtime. This includes custom entity classes like Product and Products, some AWS dependencies to APIGateway Proxy Event Request (from the artifact id aws-lambda-java-events from pom.xml), DateTime class to convert timestamp from JSON to Java object and some other classes. It sometimes takes several attempts and running the application first to find all such classes.
There are other ways to register classes for Reflection, which are described in this article Tips for writing native applications .
In the pom.xml there are a few more additional declarations necessary. First we need to use Amazon DynamoDB Client Quarkus extension from Quarkiverse (without it we run into the error) as follows:
<dependency>
<groupId>io.quarkiverse.amazonservices</groupId>
<artifactId>quarkus-amazon-dynamodb</artifactId>
<version>3.2.0</version>
</dependency>
This extension takes effect automatically, we do not have to change the source code of the application. We also need to activate the native profile as follows:
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<quarkus.native.additional-build-args>
--initialize-at-run-time=software.amazonaws.example.product.dao
</quarkus.native.additional-build-args>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
Here we initialize all classes in the software.amazonaws.example.product.dao package at runtime. Otherwise we run into the error when creating the GraalVM Native Image.
To build our application as a GraalVM native image, we have to pass the native profile as a parameter. The call then looks like this: mvn clean package -Dnative. This will save GraalVM Native Image as a file named boostrap built in function.zip.
The last part of the changes concerns AWS SAM template.yaml. Since there is no managed Lambda GraalVM runtime environment, the question arises how we can deploy our native GraalVM image on AWS Lambda. This is possible if we choose Lambda Custom Runtime as the runtime environment (this currently only supports Linux) and deploy the built .zip file as a deployment artefact. You can find out more about this in the article Building a custom runtime for AWS Lambda. This is exactly what we define in template.yaml as follows:
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
CodeUri: target/function.zip
Runtime: provided.al2023
....
With Runtime provided.al2023 we define Lambda runtime environment as Amazon Linux 2023 Custom Runtime and with CodeUri target/function.zip we define the path to the deployment artifact compiled with Maven in the previous step. Deployment works the same way with sam deploy -g. You can see how to create and query the product in the initial sample application above. Please note that the GraalVM Native Image sample application uses a different API key, namely a6ZbcDefQW12BN56WES318, which is defined in template.yaml. Please use this in your curl calls (see above for examples).
We have now examined all 5 methods and want to measure the performance of the lambda function with all methods.
Here is a summary of the methods. We will later assign the results to the corresponding method number in the table below:
- Measurement without activating Lambda SnapStart
- Measurement with activation of Lambda SnapStart, but without using the priming methods
- Measurement with activation of Lambda SnapStart and with application of priming of DynamoDB Request
- Measurement with activation of Lambda SnapStart and with application of priming of API Gateway Request Event
- Measurement with GraalVM Native Image
The results of the experiment are based on reproducing more than 100 cold starts and about 100,000 warm starts with the Lambda function GetProductByIdFunction (we ask for the already existing product with ID=1 ) for the duration of about 1 hour. We give Lambda function 1024 MB memory, which is a good trade-off between performance and cost. We also use (default) x86 Lambda architecture. For the load tests I used the load testing tool hey, which is very similar to Curl. However, you can use any tool you like, e.g. Serverless Artillery or Postman.
Before I present the performance measurements, a few notes on the scope of measurement for methods 1 to 4.
We will measure all 4 measurements with tiered compilation (which is default in Java 21, we don’t need to set anything separately) and compilation option XX:+TieredCompilation -XX:TieredStopAtLevel=1. To use the last option, you have to set it in template.yaml in JAVA_OPTIONS environment variable as follows:
Globals:
Function:
Handler: io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest
...
Environment:
Variables:
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
With these two we achieve the best performance with the different trade-offs. You can also read the article Optimizing AWS Lambda function performance for Java. This compilation option is logically irrelevant for the GraalVM Native Image.
Please also note the effect of AWS SnapStart Snapshot tiered cache. This means that in the case of SnapStart activation, we get the largest cold starts during the first measurements. Due to the tiered cache, the subsequent cold starts will have lower values. For more details about the technical implementation of AWS SnapStart and its tiered cache, I refer you to the presentation by Mike Danilov: “AWS Lambda Under the Hood”. Therefore, I will present the performance measurements for all approx. 100 cold start times (labelled as all in the table), but also for the last approx. 70 (labelled as last 70 in the table), so that the effect of Snapshot Tiered Cache becomes visible to you. Depending on how often the respective Lambda function is updated and thus some layers of the cache are invalidated, a Lambda function can experience thousands or tens of thousands of cold starts during its life cycle, so that the first longer lasting cold starts no longer carry much weight.
Performance measurement results
Before we present the measurement results, a few notes on the abbreviations:
- We will assign the results in the table below to the corresponding method number 1 to 5. See the description above
- C in the column is the abbreviation for cold start
- W in the column is the abbreviation of warm start
- P in the column is the abbreviation for percentile
- All measurements
Measurement results for tiered compilation:

Measurement results for XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation:

Measurement results for GraalVM Native Image:

Conclusion
In this article, we have explored some ways to develop, deploy and run applications on AWS Lambda using Quarkus framework and measured performance (the cold and warm start times) of the Lambda function. We also showed how we can optimize Lambda function performance using Lambda SnapStart (including various priming techniques) and GraalVM Native Image deployed as AWS Lambda Custom Runtime.
The warm start times are very acceptable even without activating SnapStart, because Java itself is a very fast programming language. However, the warm start times with GraalVM Native Image are slightly higher than when using the managed Java 21 version in AWS Lambda.
We have seen that enabling Lambda SnapStart alone reduces the cold start time significantly and even further if we implement DynamoDB request priming (method 3), which requires some implementation effort. The experimental API Gateway request priming (method 4) reduces the cold start time even further, but requires writing a lot of code, so it is up to the reader to use it or not. However, the effect of AWS SnapStart Snapshot tiered cache is clearly visible.
However, we achieve the lowest cold start times with GraalVM Native Image.
However, I would recommend the reader to start with Lambda SnapStart, especially if the priming techniques as in method 3 are applicable. SnapStart is completely managed by AWS, so we do not have to worry about the creation, signing, fault-tolerant retention and recovery of a snapshot. In the case of GraalVM, we are responsible for CI/CD ourselves. Note that with both SnapStart (in the deployment phase) and GraalVM Native Image (when creating the native image) additional time is taken (it is several minutes), so I recommend working without activating SnapStart on all environments except live/production, for example.
There is much more we can explore for performance optimisation, e.g.
- Assign different RAM to the Lambda function. With more RAM, Lambda becomes more expensive, but has more CPU power and is therefore faster. Since one of the components of Lambda pricing is GB/s, it is worthwhile to experiment with RAM settings to determine the best price/performance ratio. Especially applications on AWS Lambda deployed as GraalVM Native Image remain quite performant with less memory.
- Set different implementations of the HTTP client when creating DynamoDbClient. The default is ApacheClient, but there is also UrlConnection and AWS CRT HTTP client provided by AWS. AWS CRT Client has recently added support for GraalVM Native Image.
- Instead of default x86 Lambda architecture set arm64 (which is 25% cheaper per GB/s as x86, see AWS Lambda pricing).
All these settings result in deviations in the cold and warm start times. However, the experiments are beyond the scope of this article, but I will publish them on my personal blog page over time.