Is Quarkus the magic bullet for Java and AWS Lambda?
By: Brian Mccann
Tags: Lambda, Java, Quarkus, Serverless, Cloud Native

As you can see they are all plenty fast, with the Quarkus native flavor as the clear winner. In this case there is no “init duration” field because the function is already initialized. These are the kind of numbers we’d expect from a traditional “long running” backend application like we’d deploy with Kubernetes or on an EC2 instance. All of these averages are so fast that there kind of isn’t much to talk about. Technically the native image flavor is more than 10x as fast the Quarkus/JVM flavor, but who really cares since its only a 10ms difference. That amount of time is imperceptible to a human.

What about price ?

One compelling reason we could have for making execution times as short as possible is price. With Lambda we are billed for every millisecond of compute we use and no more. Take note also that we are not billed for function initialization time, only for execution time so although Spring Boot takes a long time to initialize, we won’t be billed for that time. In the examples above we have configured 256mb of ram. We could actually up the ram to 512mb and still pay the same price. On the Lambda pricing page you can see the increments used for billing https://aws.amazon.com/lambda/pricing/.

Let’s check to see what our bill would be for 5 million invocations assuming our function only takes 2 milliseconds to run and has under 512mb of ram configured.

5,000,000(invocations) x 2(ms) x $0.0000000083(price per ms runtime) = 8¢

That is a pretty reasonable prices for 5 million function executions. Let’s take a look at what the Spring Boot version would cost

5,000,000(invocations) x 13(ms) x $0.0000000083(price per ms runtime) = 53¢

As you can see, the price per milliseconds of running Lambda code is unbelievably cheap. Before you get too excited remember that in the real world our Lambda functions will be interacting with downstream systems that are likely not as fast. Any useful code is likely calling other APIs or interacting with external databases. To account for this I’m going to make up a more realistic number. Let’s say that our Lambda interacts with two other rest apis which it must wait on before responding to the client, bringing the total average execution time up to 800ms. Now our Lambda function costs $33 per 5 million requests. Still not bad at all. But now, if we are paying $33 a month for our Lambda function’s 5 million executions there isn’t much point in shaving off a few milliseconds. Doing so will only save us about 50 cents. My point is that the Lambda execution you will pay for typically won’t represent time that your code is processing data, it will be time that your function is blocked waiting to receive a response from another system.

Still its important to be aware of how your bill is calculated and where the costs are coming from. You may have an application that doesn’t reach out to external APIs and databases and instead does all of its processing without making additional networks calls. In this scenario it may be worth it to use Quarkus and GraalVM to get your execution time down as low as possible as it will save you money.

Why you wouldn’t use native-image

After reading this article you might think to yourself, “why would I ever choose the regular old JVM when I can have a native image that runs faster, uses less memory, and is therefore cheaper than the alternative?”. As you’ve probably guessed using the Quarkus native image feature is not without its tradeoffs. The graalVM team has put together a nice explanation of the limitations and tradeoffs involved with this technology. You can read about them here: https://github.com/oracle/graal/blob/master/substratevm/Limitations.md

One of the bigger caveats is that all code must be known at build time. I have worked with Java based tools in the past that take advantage of dynamic class loading, and it can be a pretty cool and useful feature. However, if you are operating in a Lambda environment I can’t really think of a situation where you would use this feature or if you even could. Also dynamic class loading only really makes sense for a runtime that you want to modify without shutting down, which by definition, is an execution environment that runs for a long time and Lambda definitely isn’t that. For this reason, I won’t count this limitation as a minus in the Lambda context.

This guide published by the Quarkus team goes over a few of the other gotchas you have to consider when using the native image feature with Lambda. This link takes you to closer to the end where they discuss the modifications needed for TLS.

If you want to use native image for many of the Lambda services you’d want to interact with you can’t use the SDK at out of the box and have to swap out the default http client.

Another gotcha that they don’t call out, is the amount of time it takes to build the native images. It took me between 4 and 5 minutes just to build the simple hello world Lambda. I imagine it might take a lot longer with a bigger more complicated app. Even though 4–5 minutes is not a super long time, I think it adds up when you are deploying frequently. Many of us are used to being able to build, package, and deploy our code to some sort of dev environment in well under a minute. In the case of Lambda, you can redeploy new code and have it ready to execute in a few seconds. Often the only way to test or debug a piece of functionality is to push changes over and over in a short span of time. This would get old if you had to wait for the native-image build every time.

The way I would overcome this problem would be to not deploy the native image in lower environments like dev. We can just as easily run the code with Amazon’s JRE. It’s no issue at all if we face cold starts in dev, so we need not deploy a native image there. I would however recommend using the native image in QA + TEST environments. You have to be careful about letting your non-prod environments become different from your production environments. It’s very possible that your code would behave one way running in the JVM and behave another when built as a native image.

None of these gotchas and limitations are too major but they are worth noting. When I’ve been tasked to build an application for a business they just want it done as fast as possible with as few bugs. As developers we have to ask ourselves if its really worth creating extra hurdles for ourselves when the outcome is subtle, and might not even be appreciated by the business or end users. How much of an impact adopting Quarkus will make depends on a number of factors such as the nature of the product you are building and the background and experience level of the developers on your team.

Conclusions: when I think you should and shouldn’t use Quarkus with Lambda

If you have an existing Spring Boot application that you want to move to Lambda, Quarkus with native image may be a good option. This will allow you to overcome the problem of occasionally having very slow response times due to cold starts. When switching from Spring Boot to Quarkus you will have to do a bit of refactoring. Quarkus offers a number of extensions that make it easy to keep a lot of your Spring code as is. Here is a good article about it

If you’re going to go in and refactor your application to run on Lambda anyway you should consider if you even need a framework like Spring or Quarkus. It may not be much more effort to just re-wire your app so that each RESTful route+operation combination makes use of its own Lambda function. By doing that you’ll enjoy the following benefits

each Lambda function is very simple making it easy to understand and debug

a bug or venerability in one part of your app won’t effect the rest

you’re cold start times and memory use will improve drastically without having to do any hacks to the AWS Lambda golden path. (By golden path I mean the standard recommend AWS way)

Here is an example of how you can build a Serverless Java microservice using just the Serverless framework. In my opinion this is the best way to port your application over to serverless, assuming your are willing to undertake the significant refactoring. Simply put the Serverless framework was designed from the ground up to deploy applications on Lambda, Spring and Quarkus were designed with completely different goals and assumptions.

The key distinction is that Serverless is not an application framework it’s a devOps framework. It doesn’t add any overhead to booting your application or impose any conventions on how you write your Java code. In this example multiple lambda functions are part of one code base and can be simultaneously deployed and updated with one deploy command.

If you are starting 100% greenfield I encourage you to even consider using Node over Java. I don’t think that Node is superior to Java, Java probably has more features and is more powerful in a lot of ways. But from my experience building on Lambda, the devSumer experience is smoother and more enjoyable with Node. I also think Node makes more sense for organizations going forward because Node skills are applicable to both front end and back end development. I’ve experienced and also heard about situations where backend or frontend teams have different workloads or move at different paces and end up waiting on one another so developer bandwidth goes un-utilized. It makes more sense to use a language that allows developers to be cross functional and work on any part of the stack.

I hope this article has informed you of the benefits and tradeoffs of using Quarkus and the native image functionality in the context of Lambda so that you can apply this knowledge in the context of your team and what your are trying to accomplish.

Notes on what I deployed

Here are the guides I used to deploy each different Lambda runtime flavor in case you want to try yourself.

node+express: https://mechanicalrock.github.io/2020/01/14/serverless-express-api.html

spring-boot: https://github.com/awslabs/aws-serverless-java-container/wiki/Quick-start---Spring-Boot2

Quarkus JVM version + native version: https://quarkus.io/guides/amazon-lambda-http

Here is all the code I used to conduct these experiments on gitHub https://github.com/bmccann36/lambda-runtime-flavors