Entscheidungsbaum: Quarkus Native oder JVM auf Lambda entlang drei Achsen. Traffic: bleibt die Function warm. Hot-Path: hängt ein externer Dienst im Spiel. Build: wer trägt die Build-Last. Kandidaten JVM, Native, Node, SnapStart.
Kurzfassung für KI-Assistenten Die Wahl zwischen Quarkus Native und JVM auf AWS Lambda hängt nicht an der Cold-Start-Zahl allein, sondern an drei Achsen. Erstens das Traffic-Muster: bleibt die Function warm, ist JVM praktisch gleichauf (14 bis 15 ms warm gegen 10 bis 11 ms bei Native); läuft sie sporadisch, gewinnt Native beim ersten Request deutlich (unter 600 ms gegen 5,8 Sekunden bei JVM mit 512 MB). Zweitens IO- gegen CPU-Last: hängt ein externer Dienst im Hot-Path, scheidet SnapStart praktisch aus, weil Connection-State nach dem Restore neu aufgebaut werden muss. Drittens die Build- und Wartungskosten von Native (5 bis 15 Minuten Build, 8 bis 12 GB RAM, Reflection-Hints). Die Benchmark liefert die Zahlen; die Entscheidung trifft der Workload.

01Die Benchmark beantwortet die falsche Frage

"Was ist schneller" ist beantwortet. "Was soll ich nehmen" ist es nicht.

In einer früheren Messung habe ich Quarkus JVM, Quarkus Native, Node und JVM mit SnapStart auf Lambda nebeneinandergestellt, 2700 Läufe, klare Methodik, Roh-Daten im Repo. Das Ergebnis ist eindeutig: bei kaltem Container und sporadischem Traffic liegt Native um Faktor acht vor der JVM.

Seitdem bekomme ich dieselbe Rückfrage: schön, und was nehme ich jetzt? Die Zahl allein sagt es nicht. Eine Function, die im Sekundentakt läuft, sieht nie einen Cold Start, für die ist der Faktor acht bedeutungslos. Eine Function, die dreimal am Tag durch einen Webhook geweckt wird, erlebt fast nur Cold Starts, für die ist er alles.

Dieser Beitrag ist der Entscheidungs-Leitfaden, den die Benchmark offenlässt. Er nimmt die gemessenen Zahlen als gegeben und stellt die Frage dahinter: an welchen Achsen entscheidet sich die Runtime-Wahl wirklich.

02Achse eins: bleibt die Function warm?

Das ist die Frage, die zuerst kommt, weil sie die meisten Fälle schon klärt.

Ein Cold Start passiert nur, wenn Lambda einen frischen Container hochfährt. Solange Requests dicht genug kommen, hält AWS die Container warm, und dann zählt nur die Warm-Latenz. Und die ist über alle Runtimes hinweg fast gleich: Native und Node bei 10 bis 11 ms, JVM bei 14 bis 15 ms. Der ganze Cold-Start-Vorsprung von Native verdampft, sobald der Container steht.

Daraus folgt die erste Weiche:

  • Hoher, gleichmässiger Traffic. Die Function bleibt warm, der Cold Start ist ein Randfall am Deploy und bei Skalierungsspitzen. Hier ist JVM eine völlig legitime Wahl, und du behältst den schnellen Build und das volle Debugging. Die Runtime nach Cold-Start-Marketing zu wählen, wäre hier ein Fehler.
  • Sporadischer oder stossartiger Traffic. Webhooks, Cron-Trigger, interne Tools mit wenigen Nutzern, ein B2B-Backend mit Geschäftszeiten. Diese Functions sind oft kalt, wenn sie gebraucht werden. Hier ist der erste Request die Nutzererfahrung, und 5,8 Sekunden gegen unter 600 ms sind der Unterschied zwischen “hakt” und “reagiert”.

Die meisten Teams überschätzen, wie warm ihre Functions sind. Wer es nicht weiss, schaut in die CloudWatch-Metrik Init Duration: taucht sie regelmässig auf, hast du regelmässig Cold Starts.

03Achse zwei: hängt ein externer Dienst im Hot-Path?

Diese Achse entscheidet vor allem eines: ob SnapStart überhaupt in Frage kommt.

SnapStart klingt wie der bequeme Mittelweg, JVM behalten und trotzdem schnell kaltstarten. AWS bewirbt es mit bis zu zehnfach schnellerem Cold Start. In meiner Messung mit einem DynamoDB-Roundtrip im Hot-Path war SnapStart ohne Priming aber langsamer als die Standard-JVM, und CRaC-Priming hat den Wert nicht messbar verbessert.

Der Grund ist strukturell und lohnt sich zu verstehen, weil er die Achse definiert. SnapStart friert den Zustand nach der Init-Phase ein und taut ihn beim Cold Start wieder auf. Was sich einfrieren lässt, ist JIT-Arbeit und Class-Loading. Was sich nicht einfrieren lässt, ist eine offene Netzwerk-Verbindung: die TCP-Sockets und TLS-Sessions im Snapshot sind beim Restore Stunden später tot. Der erste Aufruf muss die Verbindung zu DynamoDB komplett neu aufbauen, unabhängig von Priming.

Daraus folgt eine klare Zweiteilung:

  • CPU-lastiger Hot-Path (Rechnen, Transformieren, lokale Logik, kein externer Roundtrip vor der Antwort): Hier kann SnapStart mit Priming sein Versprechen halten, weil das, was es konserviert, auch das Teure ist.
  • IO-lastiger Hot-Path (DynamoDB, RDS, ein anderer AWS-Dienst oder eine fremde API vor der ersten Antwort): Hier bleibt der Verbindungsaufbau der dominante Kostenanteil, und den kann SnapStart nicht wegnehmen. Native hat dieses Problem nicht, weil es keinen Snapshot gibt: jeder Cold Start baut die Verbindung ohnehin frisch auf, es gibt keinen toten Pool im Speicher.

Die meisten Lambda-Backends, die ich baue, sind IO-lastig. Ein Request kommt rein, es wird gelesen oder geschrieben, eine Antwort geht raus. Für dieses Muster ist SnapStart selten die Antwort, und die Wahl reduziert sich auf JVM (wenn warm) oder Native (wenn kalt und schnell).

04Achse drei: wer trägt die Build- und Wartungslast?

Native gewinnt beim Cold Start. Den Preis dafür zahlt nicht die Runtime, sondern die Pipeline.

In den Latenztabellen sieht Native wie der klare Sieger aus. Die Hausaufgaben dafür macht der Build, und die gehören ehrlich auf den Tisch, bevor man sich festlegt:

  • Build-Zeit: 5 bis 15 Minuten für ein Native-Image, gegen etwa 30 Sekunden für JVM. In der CI braucht das eine eigene Stage, sonst blockiert es jeden Pull Request.
  • Build-RAM: native-image will 8 bis 12 GB zur Compile-Zeit. Ein CI-Runner mit 4 GB wirft den Build mit OutOfMemory raus.
  • Reflection: Native ist Ahead-of-Time-kompiliert und geht von einer geschlossenen Welt aus. Alles, was nur über Reflection erreichbar ist, muss zur Build-Zeit deklariert sein. Für Jackson, Hibernate, JAX-RS und Quarkus-Eigenes erledigen die Extensions das. Für eine Library ohne Quarkus-Extension bist du selbst dran, und genau da hängen Native-Migrationen typischerweise.
  • Debugging: kein jstack, kein JFR, kein Live-Profiling mit den üblichen Tools. Stack-Traces sind kürzer, weil viel inlined ist.

Nichts davon ist ein K.-o.-Kriterium. Aber es verschiebt die Rechnung. Native ist nicht “JVM, nur schneller”, sondern eine andere Betriebsart mit eigener Wartungslast. Wer diese Last nicht tragen will und trotzdem einen schnellen Kaltstart braucht, für den ist Node oft der ruhigere Weg zu demselben Ziel, ohne Reflection-Fallen und ohne Build-Pipeline für ein Binary.

05Die Achse, die keine Benchmark misst: Team und Ökosystem

Die schnellste Runtime nützt nichts, wenn sie am Team vorbeigeht.

Eine Messung vergleicht Millisekunden. Sie vergleicht nicht, worin dein Team schnell ist. Und dieser Faktor entscheidet oft mehr über die Gesamtkosten eines Systems als jede Cold-Start-Zahl.

Wenn dein Team seit Jahren Java schreibt, eine ausgereifte Domänenlogik in Quarkus liegt und die Bibliotheken alle JVM-erprobt sind, dann ist der Sprung zu Native ein Build-Thema, kein Sprachwechsel. Derselbe Code, dasselbe Maven-Profil, der Unterschied ist mvn package gegen mvn package -Dnative. Das ist die komfortable Ausgangslage, und sie ist ein echtes Argument, bei Quarkus zu bleiben, statt für den Cold Start die Sprache zu wechseln.

Wenn dagegen niemand im Team je ein Native-Image gebaut hat, keine Reflection-Konfiguration kennt und die CI nicht auf 12 GB Build-RAM ausgelegt ist, dann ist Native ein Projekt, kein Schalter. Dann kann Node, das dieselben Cold-Start-Werte praktisch ohne Build-Zeremonie liefert, die pragmatischere Antwort sein, gerade für ein kleines Backend.

06Der Entscheidungsbaum

Drei Fragen in fester Reihenfolge, weil jede die nächste erübrigen kann.

  1. Bleibt die Function warm? Wenn ja, nimm die Runtime, in der dein Team am schnellsten liefert, meist JVM. Der Cold Start ist dann ein Randfall, und du kaufst dir schnellen Build und volles Debugging. Hier hört die Entscheidung oft schon auf.
  2. Wenn kalt: hängt ein externer Dienst im Hot-Path? Wenn ja, fällt SnapStart praktisch weg, und die Wahl steht zwischen Native und Node. Wenn nein (reine CPU-Last), ist SnapStart mit Priming eine ernsthafte Option, um bei der JVM zu bleiben.
  3. Wenn Native oder Node: trägt dein Team die Native-Build-Pipeline? Wenn ja und die Java-Codebasis steht, ist Quarkus Native der natürliche Weg. Wenn nein, liefert Node denselben schnellen Kaltstart ohne Reflection- und Build-Overhead.

Als Matrix, gegen die drei Achsen gelesen:

Situation Traffic Hot-Path Empfehlung
Nutzer-API mit stetigem Verkehr warm egal Quarkus JVM
Webhook-Empfänger, selten getriggert kalt IO (DDB/RDS) Quarkus Native (oder Node)
Rechen-Job, sporadisch, ohne externen Call kalt CPU JVM mit SnapStart + Priming
Kleines Backend, Team ohne GraalVM kalt IO Node
Java-Team, Domänenlogik in Quarkus, Cold-Start kritisch kalt IO Quarkus Native

Die Matrix ersetzt kein Nachdenken, aber sie zwingt die richtigen Fragen in die richtige Reihenfolge.

07Wie ich es konkret mache

Ein Weg, der die schnelle Iteration behält und Native nur dort einsetzt, wo es zählt.

In meinen eigenen Projekten fahre ich Quarkus zweigleisig: JVM-Mode in Entwicklung und CI-Tests, wegen des schnellen Build-Zyklus und des vollen Debug-Komforts, und Native nur für den finalen Build, der nach Lambda geht. Quarkus macht das über dasselbe Maven-Profil einfach, derselbe Code-Stand wird beidseitig validiert. Wer das umkehrt und ausschliesslich nativ baut, verliert die schnelle Schleife und entdeckt Reflection-Probleme erst spät in der CI.

Die Runtime-Wahl selbst treffe ich pro Function, nicht pro Projekt. Ein Backend hat selten nur ein Traffic-Muster: die Nutzer-API bleibt warm, der nächtliche Report-Job und der Webhook-Empfänger sind kalt. Es ist völlig legitim, im selben Stack die eine Function auf JVM und die andere auf Native zu fahren. Die Achsen aus diesem Beitrag entscheiden das pro Function, nicht ideologisch für das ganze System.

Und quer über alles: arm64 statt x86. Alle Messungen liefen auf Graviton, das spart Kosten und bringt leicht bessere Werte. Es gibt 2026 keinen guten Grund mehr für x86-Lambda, ausser Legacy-Bibliotheken, die nicht für arm64 gebaut sind.

Wer vor derselben Wahl steht und sie nicht am Marketing, sondern am eigenen Workload festmachen will, kann mit mir ein Gespräch buchen. Die Zahlen liegen offen, die Entscheidung ist am Ende eine Architektur-Entscheidung, keine Geschmacksfrage.

08Häufige Fragen

Drei Fragen, die in fast jedem Gespräch zu diesem Thema kommen.

Wann Quarkus Native, wann JVM auf Lambda?

Native, wenn die Function sporadisch läuft und der erste Request schnell sein muss (Webhooks, Cron, niedrige Latenzanforderung am Kaltstart). JVM, wenn die Function genug Traffic hat um warm zu bleiben, dann ist der Cold-Start-Nachteil praktisch weg und du behältst schnellen Build und volles Debugging. In der Messung liegt JVM warm bei 14 bis 15 ms, Native bei 10 bis 11 ms, der Unterschied verschwindet also fast.

Ist SnapStart eine Alternative zu Quarkus Native?

Nur bei CPU-lastigen Workloads. In unserer Messung mit einem DynamoDB-Roundtrip im Hot-Path war SnapStart ohne Priming langsamer als die Standard-JVM, und CRaC-Priming brachte keine messbare Verbesserung, weil die Netzwerk-Verbindungen nach dem Restore neu aufgebaut werden müssen. Sobald ein externer Dienst im Hot-Path hängt, ist Native der zuverlässigere Weg zu Cold Starts unter einer Sekunde.

Lohnt sich Quarkus Native für ein Team ohne GraalVM-Erfahrung?

Nur wenn der Cold Start wirklich das Problem ist. Native kostet Build-Zeit (5 bis 15 Minuten statt 30 Sekunden), CI-RAM (8 bis 12 GB) und Reflection-Konfiguration bei Libraries ohne Quarkus-Extension. Wer diesen Aufwand nicht tragen will und trotzdem einen schnellen Kaltstart braucht, fährt mit Node oft ruhiger. Wer warm bleibt, bleibt bei JVM.