Finally! It’s time to forget about those cold starts and scheduled warm-up calls. Simply deploy Java (or Kotlin) natively compiled code to AWS Lambda! Or maybe not yet? The post is an attempt to answer this question with an experiment like:
- deploy natively compiled (by GraalVM native-image tool) Kotlin and Java lambdas to AWS cloud
- have the same setup with Node.js lambda nearby
- bridge incoming calls through AWS API Gateway
- run basic tests to measure cold start timings and see behaviour after warm-up
Project boilerplate has been generated with Micronaut tooling and provisioned to AWS infrastructure using Serverless framework. Load testing delivered with K6. Feel free to explore related step-by-step setup guide to deploy the same setup and play with load tests.
- each lambda deployed 10 times (which guarantees to have init run for the first call after deployment)
- the deployment followed by 10 seconds load test. The load test is strictly sequential, all requests issued one after another.
- client-side statistics captured from K6 log output, AWS-side figures taken from CloudWatch Logs Insights with simplest queries.
- each lambda deployed once and called to trigger initial iteration
- deployment followed by 10 iterations, each with the same 10 seconds sequential load test
- “cold” start outliers are clearly visible. Their value reaches ~7.5 sec with no effort
- on a bright side, other min/max/percentile figures distributed quite evenly across tested lambdas. JVM/native max numbers leaning a little towards bigger values
- these slight fluctuations could be caused by the network layer as well. Next AWS-side measurements section might be used for cross-checks.
- Node.js min numbers 0.84/0.79 ms(!) are invisible at this log-scale chart
- alas, JVM-based lambdas show “cold” start numbers up to 5 sec. Add ~2.5 sec init time on top of this. Predictably sad
- native Kotlin/Java lambdas’ performance looks fairly acceptable. Even the worst iteration kept close to 180 ms
- it is worth to mention that enterprise GraalVM version features an opportunity to tweak performance with profile-guided optimizations. At the moment GraalVM Enterprise Edition licensed for free testing, evaluation, or for developing non-production applications only. Still, there is a hope to see this tooling within CE version as well…
- init timings for native images (~330 ms) are much closer to Node.js ones (~160 ms)
- Java/Kotlin JVM lambdas are still experiencing “cold” start issues. These ~5–7 seconds is just a matter of fact today. Further manipulations with artefact size and classpath might help with init timings, but…
- GraalVM native lambdas have sustainable performance profile. Init time (~330ms) comparable to Node.js reference lambda (~160ms) and way better than for JVM (~2.5 sec)
- At the same time, GraalVM/Micronaut/Serverless combo instantly leverages from Java/Kotlin existing ecosystem, reach tooling and active community
- Node.js timings are really good and very stable. Median is barely different from 99%-ile
- in fact, Java and Kotlin -based native images have the same size, 14.62 MB. JVM images sizing is a little different: 38.58 MB (Java) and 44.37 MB (Kotlin). And Node.js size is just 297 bytes :)
- Last but not least, this project is intentionally bent from autogenerated Micronaut template. This short journey has proven two things. #1: a lot of issues may arise during migration to *.gradle.kts, restructuring multi-module Gradle project and native-image configuration tweaks. #2: either these issues solved multiple times or solvable with reasonable efforts.
You may run described deployments and tests using project’s github repo.