Quarkus 3 application on AWS Lambda- Part 1 Introduction to the sample application and first Lambda performance measurements
What will we explore and learn in this article series? In this article series, we will explore some ways to develop, deploy and run applications on AWS Lambda using the 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 optimize 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. 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. I used GraalVM 23 for my measurements, but you can use the newest version. 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 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 only 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 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 { re

What will we explore and learn in this article series?
In this article series, we will explore some ways to develop, deploy and run applications on AWS Lambda using the 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 optimize 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.
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. I used GraalVM 23 for my measurements, but you can use the newest version.
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 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 only 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 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,
io.quarkus
quarkus-amazon-lambda
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.
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 For it I used the load test tool hey, but you can use whatever tool you want, like Serverless-artillery or Postman.
We will measure 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
#SnapStart:
#ApplyOn: PublishedVersions
...
Environment:
Variables:
JAVA_TOOL_OPTIONS: "-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
Also we'd like to measure the Lambda performance without SnapStart being activated for the Lambda function first. So make sure that two corresponding lines are commented out as stated above.
Cold (c) and warm (w) start time with tiered compilation in ms:
c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|
3344 | 3422 | 3494 | 3633 | 3904 | 3907 | 5.92 | 6.83 | 8.00 | 19.46 | 50.44 | 1233 |
Cold (c) and warm (w) start time with -XX:+TieredCompilation -XX:TieredStopAtLevel=1 compilation in ms:
c p50 | c p75 | c p90 | c p99 | c p99.9 | c max | w p50 | w p75 | w p90 | w p99 | w p99.9 | w max |
---|---|---|---|---|---|---|---|---|---|---|---|
3357 | 3456 | 3554 | 4039 | 4060 | 4060 | 6.01 | 6.83 | 8.13 | 19.77 | 53.74 | 1314 |
Conclusion
In the first part of our series about how to develop, run and optimize Quarkus web application on AWS Lambda, we demonstrated how to write a sample application which uses the Quarkus framework, AWS Lambda, Amazon API Gateway and Amazon DynamoDB. We also made the first Lambda performance (cold and warm start time) measurements and observed quite a big cold start time. In the next parts of the series we'll introduce approaches and techniques to reduce the Lambda cold start time with AWS SnapStart (including various priming techniques) and GraalVM Native Image and also measure their impact on the Lambda warm start time. Stay tuned!