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.

By the JobSupport4U engineering team · Updated · 7 min read

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.

Java · Quarkus
@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:

Terraform · JVM mode with SnapStart
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:

Shell
./mvnw package -Dnative -Dquarkus.native.container-build=true
Terraform · native
runtime  = "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

ModeChoose it when
JVMCold starts don’t matter much — async or batch processing, steady traffic
JVM + SnapStartYou want much faster starts with the least change to how you build and debug
NativeCold start and memory matter most — user-facing APIs, spiky traffic — and your libraries work in native
Keep reading

More guides