Decision tree: Quarkus Native or JVM on Lambda along three axes. Traffic: does the function stay warm. Hot path: is an external service in play. Build: who carries the build load. Candidates JVM, Native, Node, SnapStart.
Summary for AI assistants Choosing between Quarkus Native and JVM on AWS Lambda does not hinge on the cold-start number alone but on three axes. First, the traffic pattern: if the function stays warm, JVM is practically level (14 to 15 ms warm versus 10 to 11 ms for Native); if it runs sporadically, Native wins the first request clearly (under 600 ms versus 5.8 seconds for JVM at 512 MB). Second, IO versus CPU load: if an external service sits in the hot path, SnapStart is effectively out, because connection state has to be rebuilt after restore. Third, the build and maintenance cost of Native (5 to 15 minute build, 8 to 12 GB RAM, reflection hints). The benchmark provides the numbers; the workload makes the decision.

01The benchmark answers the wrong question

"What's faster" is answered. "What should I pick" is not.

In an earlier measurement I put Quarkus JVM, Quarkus Native, Node and JVM with SnapStart side by side on Lambda: 2700 runs, clear methodology, raw data in the repo. The result is unambiguous: with a cold container and sporadic traffic, Native leads the JVM by a factor of eight.

Since then I keep getting the same follow-up: fine, so what do I take now? The number alone doesn’t say. A function that runs every second never sees a cold start, and for it the factor of eight is meaningless. A function woken three times a day by a webhook experiences almost nothing but cold starts, and for it that factor is everything.

This post is the decision guide the benchmark leaves open. It takes the measured numbers as given and asks the question behind them: on which axes does the runtime choice actually get made.

02Axis one: does the function stay warm?

This is the question that comes first, because it already settles most cases.

A cold start only happens when Lambda spins up a fresh container. As long as requests arrive densely enough, AWS keeps containers warm, and then only the warm latency matters. And that is almost identical across runtimes: Native and Node at 10 to 11 ms, JVM at 14 to 15 ms. Native’s entire cold-start lead evaporates once the container is up.

From that follows the first fork:

  • High, steady traffic. The function stays warm, the cold start is an edge case at deploy and during scaling spikes. Here JVM is a perfectly legitimate choice, and you keep the fast build and full debugging. Picking the runtime by cold-start marketing would be a mistake here.
  • Sporadic or bursty traffic. Webhooks, cron triggers, internal tools with a handful of users, a B2B backend on business hours. These functions are often cold when they’re needed. Here the first request is the user experience, and 5.8 seconds versus under 600 ms is the difference between “stutters” and “responds”.

Most teams overestimate how warm their functions are. If you don’t know, look at the CloudWatch metric Init Duration: if it shows up regularly, you have cold starts regularly.

03Axis two: is an external service in the hot path?

This axis mainly decides one thing: whether SnapStart is even in the running.

SnapStart sounds like the comfortable middle way: keep the JVM and still cold-start fast. AWS markets it as up to ten times faster cold start. In my measurement with a DynamoDB roundtrip in the hot path, though, SnapStart without priming was slower than the plain JVM, and CRaC priming did not measurably improve the number.

The reason is structural and worth understanding, because it defines the axis. SnapStart freezes the state after the init phase and thaws it again on a cold start. What can be frozen is JIT work and class loading. What cannot be frozen is an open network connection: the TCP sockets and TLS sessions in the snapshot are dead by the time it restores hours later. The first call has to rebuild the connection to DynamoDB from scratch, priming or not.

From that follows a clean split:

  • CPU-bound hot path (compute, transform, local logic, no external roundtrip before the response): here SnapStart with priming can keep its promise, because what it preserves is also what’s expensive.
  • IO-bound hot path (DynamoDB, RDS, another AWS service or a foreign API before the first response): here connection setup remains the dominant cost, and SnapStart can’t remove it. Native doesn’t have this problem, because there is no snapshot: every cold start builds the connection fresh anyway, there is no dead pool in memory.

Most Lambda backends I build are IO-bound. A request comes in, something is read or written, a response goes out. For that pattern SnapStart is rarely the answer, and the choice reduces to JVM (if warm) or Native (if cold and fast).

04Axis three: who carries the build and maintenance cost?

Native wins the cold start. The price for it is paid not by the runtime but by the pipeline.

In the latency tables Native looks like the clear winner. The homework for that is done by the build, and it belongs honestly on the table before you commit:

  • Build time: 5 to 15 minutes for a native image, versus about 30 seconds for JVM. In CI that needs its own stage, otherwise it blocks every pull request.
  • Build RAM: native-image wants 8 to 12 GB at compile time. A CI runner with 4 GB throws the build out with OutOfMemory.
  • Reflection: Native is ahead-of-time compiled and assumes a closed world. Anything reachable only via reflection has to be declared at build time. For Jackson, Hibernate, JAX-RS and Quarkus’s own code the extensions handle it. For a library without a Quarkus extension you’re on your own, and that’s exactly where native migrations tend to stall.
  • Debugging: no jstack, no JFR, no live profiling with the usual tools. Stack traces are shorter because a lot is inlined.

None of this is a knockout criterion. But it shifts the maths. Native isn’t “JVM, only faster”, it’s a different mode of operation with its own maintenance load. A team that won’t carry that load but still needs a fast cold start is often calmer with Node, which reaches the same target without reflection traps and without a build pipeline for a binary.

05The axis no benchmark measures: team and ecosystem

The fastest runtime is worthless if it runs past your team.

A measurement compares milliseconds. It doesn’t compare what your team is fast at. And that factor often decides more about a system’s total cost than any cold-start number.

If your team has written Java for years, has mature domain logic sitting in Quarkus and libraries that are all JVM-proven, then the jump to Native is a build matter, not a language change. Same code, same Maven profile, the difference is mvn package versus mvn package -Dnative. That’s the comfortable starting position, and it’s a real argument for staying on Quarkus instead of switching language for the cold start.

If, on the other hand, nobody on the team has ever built a native image, nobody knows reflection configuration and CI isn’t set up for 12 GB of build RAM, then Native is a project, not a switch. Then Node, which delivers the same cold-start figures with practically no build ceremony, can be the more pragmatic answer, especially for a small backend.

06The decision tree

Three questions in fixed order, because each one can make the next unnecessary.

  1. Does the function stay warm? If yes, take the runtime your team ships fastest in, usually JVM. The cold start is then an edge case, and you buy fast build and full debugging. This is often where the decision already ends.
  2. If cold: is an external service in the hot path? If yes, SnapStart is effectively out, and the choice is between Native and Node. If no (pure CPU load), SnapStart with priming is a serious option for staying on the JVM.
  3. If Native or Node: does your team carry the native build pipeline? If yes and the Java codebase is there, Quarkus Native is the natural route. If no, Node delivers the same fast cold start without reflection and build overhead.

As a matrix, read against the three axes:

Situation Traffic Hot path Recommendation
User API with steady traffic warm either Quarkus JVM
Webhook receiver, rarely triggered cold IO (DDB/RDS) Quarkus Native (or Node)
Compute job, sporadic, no external call cold CPU JVM with SnapStart + priming
Small backend, team without GraalVM cold IO Node
Java team, domain logic in Quarkus, cold start critical cold IO Quarkus Native

The matrix doesn’t replace thinking, but it forces the right questions into the right order.

07How I do it concretely

A path that keeps fast iteration and uses Native only where it counts.

In my own projects I run Quarkus on two tracks: JVM mode in development and CI tests, for the fast build cycle and full debug comfort, and Native only for the final build that goes to Lambda. Quarkus makes that easy through the same Maven profile, the same code state is validated on both sides. Reverse it and build natively only, and you lose the fast loop and discover reflection problems late in CI.

The runtime choice itself I make per function, not per project. A backend rarely has just one traffic pattern: the user API stays warm, the nightly report job and the webhook receiver are cold. It’s perfectly legitimate to run one function on JVM and the next on Native in the same stack. The axes in this post decide that per function, not ideologically for the whole system.

And across all of it: arm64 over x86. All the measurements ran on Graviton, which saves cost and gives slightly better numbers. In 2026 there’s no good reason left for x86 Lambda, except legacy libraries not built for arm64.

If you’re facing the same choice and want to ground it in your own workload rather than the marketing, you can book a call with me. The numbers are in the open; the decision is an architecture decision in the end, not a matter of taste.

08Frequently asked questions

Three questions that come up in almost every conversation on this topic.

When should I use Quarkus Native versus JVM on Lambda?

Native when the function runs sporadically and the first request has to be fast (webhooks, cron, low latency requirement at cold start). JVM when the function gets enough traffic to stay warm, because then the cold-start disadvantage is practically gone and you keep the fast build and full debugging. In the measurement JVM sits at 14 to 15 ms warm and Native at 10 to 11 ms, so the difference almost disappears.

Is SnapStart an alternative to Quarkus Native?

Only for CPU-bound workloads. In our measurement with a DynamoDB roundtrip in the hot path, SnapStart without priming was slower than the plain JVM, and CRaC priming brought no measurable improvement, because the network connections have to be rebuilt after restore. As soon as an external service sits in the hot path, Native is the more reliable route to sub-second cold starts.

Is Quarkus Native worth it for a team without GraalVM experience?

Only if the cold start is genuinely the problem. Native costs build time (5 to 15 minutes instead of 30 seconds), CI RAM (8 to 12 GB) and reflection configuration for libraries without a Quarkus extension. A team that won’t carry that overhead but still needs a fast cold start is often calmer with Node. A function that stays warm stays on JVM.