Quarkus · AWS Lambda
Quarkus on AWS Lambda: JVM vs SnapStart vs native image
Run Quarkus on AWS Lambda with fast cold starts: when to use JVM mode, SnapStart or a native image, how to write handlers, deploy with Terraform and measure results.
Java on AWS Lambda has a reputation for slow cold starts. Quarkus is one of the best tools for fixing that — but it gives you three different ways to run, each with different trade-offs. This guide explains when to use each, and how to set them up.
Why Quarkus suits Lambda
A cold start happens when Lambda creates a new execution environment: it starts the runtime and runs your initialization code before the first request. A typical Java framework does a lot of work at that moment — classpath scanning, reflection, dependency wiring. Quarkus does most of that at build time, so there is far less to do when the function starts.
Three ways to run: JVM, SnapStart, native
- JVM mode — a normal Java runtime (for example
java21). Simplest to build and debug, full Java ecosystem, but the slowest cold start of the three. - JVM mode with SnapStart — Lambda initializes your function once when you publish a version, snapshots the memory, and restores from that snapshot on cold start. Much faster starts while keeping the normal JVM.
- Native image — Quarkus compiles your function ahead of time with GraalVM (or Mandrel) into a native executable that runs on a custom runtime. The fastest cold start and the lowest memory use, at the cost of longer builds and some native-image restrictions.
Writing the handler
Add the quarkus-amazon-lambda extension and implement the standard Lambda RequestHandler. Inject your services with CDI as usual. If a project contains several handlers, choose the active one with the quarkus.lambda.handler property, matching the @Named value.
@Named("orders")
public class OrderHandler implements RequestHandler<SQSEvent, SQSBatchResponse> {
@Inject
OrderService orders;
@Override
public SQSBatchResponse handleRequest(SQSEvent event, Context context) {
List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>();
for (SQSEvent.SQSMessage msg : event.getRecords()) {
try {
orders.process(msg.getBody());
} catch (Exception e) {
failures.add(new SQSBatchResponse.BatchItemFailure(msg.getMessageId()));
}
}
return new SQSBatchResponse(failures);
}
}For HTTP APIs behind API Gateway, the quarkus-amazon-lambda-http extension lets you write ordinary Jakarta REST endpoints instead of a raw handler.
Building and deploying
A Quarkus Lambda build produces target/function.zip. In JVM mode, point Lambda at Quarkus’s stream handler and enable SnapStart on published versions:
resource "aws_lambda_function" "orders" {
function_name = "orders"
runtime = "java21"
handler = "io.quarkus.amazon.lambda.runtime.QuarkusStreamHandler::handleRequest"
filename = "target/function.zip"
role = aws_iam_role.lambda.arn
memory_size = 1024
timeout = 30
publish = true
snap_start {
apply_on = "PublishedVersions"
}
}For a native build, compile inside a container so the executable is built for Linux even on Windows or macOS, then deploy it on a custom runtime. The handler value isn’t used by custom runtimes, but Lambda requires one:
./mvnw package -Dnative -Dquarkus.native.container-build=trueruntime = "provided.al2023"
handler = "not.used.in.provided.runtime"
filename = "target/function.zip"SnapStart caveats
- SnapStart applies to published versions (and aliases pointing to them), not to
$LATEST— invoke through a version or alias. - Anything created during initialization is shared by every restored environment: avoid generating unique IDs, random seeds or credentials at init time.
- Network connections opened during initialization may be stale after restore; make sure clients reconnect cleanly.
Native image caveats
- Reflection must be known at build time. Quarkus extensions handle this for the libraries they support; for your own classes used reflectively (for example by a JSON library), add
@RegisterForReflection. - Native builds are slow and memory-hungry — build them in CI, and develop in JVM mode.
- Test the native executable itself, not just JVM mode: some failures only appear in the native build.
Measure, don’t guess
Every cold start writes a REPORT line to CloudWatch Logs with an Init Duration (or Restore Duration with SnapStart). Compare those numbers for your real function in each mode, and remember that memory size also sets CPU: a function with more memory often initializes noticeably faster.
Which mode to choose
| Mode | Choose it when |
|---|---|
| JVM | Cold starts don’t matter much — async or batch processing, steady traffic |
| JVM + SnapStart | You want much faster starts with the least change to how you build and debug |
| Native | Cold start and memory matter most — user-facing APIs, spiky traffic — and your libraries work in native |