TL;DR: Project Leyden Changes Java’s Cold-Start Game
- What: An AOT cache built into OpenJDK (JEP 483 in JDK 24; JEPs 514 & 515 in JDK 25; JEP 516 in JDK 26), integrated into Quarkus 3.32
- Impact: ~78% faster startup on a minimal REST app (370 ms → 80 ms) and roughly 3 s → under 1 s on a 9,000-class CRUD app, with zero code changes
- Cost: Container images grow by ~40 MB on a small app and up to ~200 MB on a large one; the cache layer never benefits from Docker layer reuse
- What it is not: Not native image. The JIT stays, peak throughput is unchanged, warmup is shortened rather than eliminated
- Status: Supported in Quarkus 3.32+ on JDK 25+; JDK 26 adds ZGC support via JEP 516
The Java Cold-Start Problem
Java has a reputation problem: it’s slow to start. Project Leyden changes that equation by building an ahead-of-time (AOT) cache into OpenJDK, cutting the cold-start penalty that has haunted Java deployments for decades.
Your containerized microservice takes 370 milliseconds just to boot on a fresh pod. Your serverless function waits 500+ ms for the JVM to initialize before it runs a line of your code. When traffic spikes and Kubernetes spins up 50 new replicas, your users see a thundering herd of slow initializations before the load balancer routes traffic to the warm ones.
For years, the industry’s answer was: go native. Compile to machine code with GraalVM, and trade some peak throughput for startup speed. But native isn’t free — it assumes a closed world, so reflection, dynamic proxies and resource loading all need explicit configuration; build times lengthen considerably; and CPU-bound workloads can give up throughput, commonly in the 10–20% range with profile-guided optimisation and more without it.
Project Leyden takes a different route. It shifts class loading, linking and method profiling into an AOT cache produced by a training run — while keeping the full JVM, the JIT, and the open-world assumptions your libraries rely on. Both approaches do their work before deployment; the difference is what survives into production. Leyden ships a normal JVM plus a cache. Native image ships a static binary with no JIT.
According to Quarkus’s March 2026 integration report, a small REST API that took 370 ms to start with a stock JVM now boots in 80 ms — a 78% win — on plain OpenJDK, not a proprietary compiler.
For a platform running hundreds of microservices, that is transformative.
What Is Project Leyden?
A Series of JEPs, Not a Single Feature
Project Leyden is a series of Java Enhancement Proposals rolling into the JDK across releases. Here’s what has landed across JDK 24–26:
- JEP 483: Ahead-of-Time Class Loading & Linking (JDK 24) — A training run records which classes your application loads and stores them pre-linked in an AOT cache, so the JVM skips that work on every subsequent start.
- JEP 514: Ahead-of-Time Command-Line Ergonomics (JDK 25) — Collapses the old record-then-create two-step into a single
-XX:AOTCacheOutputflag. - JEP 515: Ahead-of-Time Method Profiling (JDK 25) — Stores execution profiles from the training run so the JIT knows which methods are hot and compiles them immediately instead of rediscovering them.
- JEP 516: Ahead-of-Time Object Caching with Any GC (JDK 26) — Makes the cached heap objects GC-agnostic, which is what finally brings ZGC into the picture.
The net effect: the JVM learns which classes your application loads and which methods run hot, then reuses that knowledge on every cold start.
📌 What the cache does not contain (yet)
The AOT cache holds loaded and linked classes, method profiles and heap objects — not compiled machine code. Caching JIT output is still on the roadmap. In practice this means warmup is shortened (the JIT compiles the right methods immediately rather than discovering them) but not eliminated. Descriptions of Leyden as “replaying cached JIT code” are running ahead of what ships today.
Why the Open World Matters
GraalVM Native Image performs a closed-world analysis: every reachable class must be known at build time, and anything discovered dynamically — reflection, proxies, serialization, resource bundles — must be declared up front. Leyden makes no such demand. The cache is an accelerator for a normal JVM; if a class was not in the training run, it simply loads the slow way.
That difference is why Leyden appeals to banks and enterprises: you get faster cold starts without sacrificing the observability, tooling and throughput guarantees that make JVM platforms enterprise-grade. Everything that worked yesterday — JFR, heap dumps, agents, your APM vendor’s bytecode instrumentation — still works.
Benchmark Results from Quarkus 3.32+
In March 2026 the Quarkus team published their integration numbers alongside the 3.32 release announcement. They are explicit that the figures were measured on developer laptops rather than in an isolated lab, so treat them as directional. But they are consistent across two real application shapes.
Small REST API
A minimal REST endpoint with HTTP handling:
- Fast-JAR (stock JVM): 370 ms startup | 122 MB RSS | 456 MB image
- Project Leyden AOT cache: 80 ms startup | 103 MB RSS | 495 MB image
- Native (GraalVM): 17 ms startup | 37 MB RSS | 155 MB image
Leyden wins the startup race by 78% over the stock JVM, and — less expected — trims RSS as well, because the pre-linked classes are memory-mapped from the cache rather than parsed into fresh metaspace. Native is still faster to boot and far lighter, at the cost of the closed world.
Large CRUD Application — the Case That Matters
A realistic microservice with Hibernate, database connections, and request handling:
- Fast-JAR (stock JVM): ~3,000 ms startup | 517 MB image
- Project Leyden AOT cache: ~900 ms startup | 715 MB image
The release announcement states it plainly: a large REST CRUD application with 9,000 classes “goes from 3 seconds to 900 ms.” This is the number that should drive your decision. Cutting roughly two seconds off a heavyweight service matters far more than shaving 290 ms off a trivial one — and it is the heavyweight services that dominate a bank’s estate.
The Image Size Trade-off
Leyden does not hide its cost: it makes your container image bigger. For the small app, the cache adds 39 MB (+8.5%). For the large app, the image grows from 517 MB to 715 MB (+38%) — the cache footprint scales with the number of classes and the size of the archived heap.
That is the Leyden bargain: you trade disk and registry bandwidth for startup latency and memory. In a cloud-native platform where cold-start variance matters more than image size, that is a good deal.
When to Use It (and When Not To)
✅ A Good Fit For:
- Containerized microservices on Kubernetes — horizontal scaling events spin up new pods regularly; Leyden cuts the latency spike on each cold pod.
- Serverless Java functions — AWS Lambda, Google Cloud Functions, Azure Functions. Cold-start latency is money; shaving hundreds of milliseconds per invocation adds up fast.
- Rolling and canary deployments — every new pod is a cold start; Leyden shortens the window of reduced capacity during each wave.
- JVM applications where peak throughput matters — the JIT stays in place, so steady-state performance is unchanged. If you are weighing that against native, Quarkus Native vs JIT: the banking reality check walks through the same trade-off with production numbers.
- Observability-first teams — you keep the full JVM diagnostic stack: JFR, heap and thread dumps, async-profiler, and agent-based APM. Native image’s diagnostic gaps do not apply.
❌ Probably Not Worth It If:
- Your pods live for hours or days — if startup only happens once per deployment cycle, the cold-start improvement is irrelevant.
- Your image size is critical — Leyden adds 8–40% to the image. If you ship to embedded systems or bandwidth-constrained edge networks, native’s 155 MB might be the call.
- You need sub-20 ms startup — native still owns the ultra-low-latency crown. Leyden gets a small service to roughly 80 ms; if your budget is 20 ms, native is the only option.
- You are pinned to JDK 17 or 21 — Leyden requires JDK 25+. If your CI/CD, app server or vendor certification is hardwired to an older JDK, this is a planning item rather than a project.
Leyden vs. GraalVM Native: The Trade-Off Matrix
Both attack the cold-start problem from different ends, and neither is universally better. Startup, memory and image figures come from the Quarkus measurements; the throughput and tooling rows are practitioner judgement, not benchmark results.
| Dimension | Project Leyden (JDK 25+) | GraalVM Native |
|---|---|---|
| Startup time (small REST) | 80 ms | 17 ms (4.7× faster) |
| Startup time (large CRUD) | ~900 ms (from ~3,000 ms) | Not published for this app |
| Peak throughput | 100% (full JVM, JIT intact) | Typically 80–90% with PGO; lower without it (workload-dependent) |
| Memory (RSS, small REST) | 103 MB | 37 MB |
| Image size (small REST) | 495 MB; cache adds ~40 MB minimum and grows with the app | 155 MB |
| Observability (JFR, dumps, profilers) | Full JVM suite | Limited (no bytecode-instrumented events) |
| Code changes required | Zero | Reflection/resource configuration required; minimal on supported frameworks |
| Build time | One extra training run (seconds) | Minutes; memory-hungry native build |
| Licensing (production) | Open, GPL/OpenJDK | CE is open source; Oracle GraalVM free for production under GFTC |
The verdict: Leyden is the pragmatist’s choice — most of the cold-start win without sacrificing throughput, observability or the open world. Native is still faster to boot and smaller in memory, but the cost is higher. For enterprises running mixed workloads — batch, real-time APIs, steady-state services — Leyden is usually the better ROI.
Getting Started with Quarkus 3.32+
If you’re running Quarkus, Leyden is already available. Here’s how to enable it:
Prerequisites
- Quarkus 3.32 or later
- JDK 25 or later (JDK 26 if you run ZGC)
- An integration-test suite that exercises the paths production traffic actually takes
Step 1: Enable the AOT Cache
Add one property to application.properties:
quarkus.package.jar.aot.enabled=true
Quarkus treats this as a flag on the standard JAR packaging rather than a separate packaging type.
Step 2: Build With a Training Run
The training run happens during your integration tests, so the cache must be produced by the same lifecycle invocation that builds the artifact. Run one command — do not follow verify with a separate package, because package runs earlier in the Maven lifecycle and a subsequent clean package will wipe target/ and rebuild without the cache:
# Maven
./mvnw verify -Dquarkus.package.jar.aot.enabled=true -DskipITs=false
# Gradle
./gradlew build quarkusIntTest -Dquarkus.package.jar.aot.enabled=true
Quarkus records which classes load and which methods run hot, and writes the AOT cache into the build output alongside your application.
Step 3: Build the Image and Deploy
docker build -f src/main/docker/Dockerfile.jvm -t myapp:leyden .
docker run myapp:leyden
On cold start the JVM maps the AOT cache instead of loading and linking every class from scratch.
Step 4: Verify the Cache Is Actually Being Used
A silently rejected cache is the most common way a team “enables Leyden” and sees no improvement — the deployment succeeds and the benefit quietly vanishes. Two flags make that impossible to miss:
# Fail fast instead of silently falling back to a cold start
java -XX:AOTMode=on -XX:AOTCache=app.aot -jar app.jar
# Log what the runtime actually mapped
java -Xlog:aot -jar app.jar
Run -XX:AOTMode=on in staging so a broken cache fails the deployment rather than quietly degrading it. In production, leave the default (auto) so a stale cache after an emergency JDK patch degrades gracefully instead of taking the service down.
Example: A Plain Quarkus REST Endpoint
Nothing about this code is Leyden-aware — that is the point:
package com.example;
import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@Singleton
@Path("/api/hello")
public class GreetingResource {
@GET
@Produces("application/json")
public Greeting hello() {
return new Greeting("Hello from Leyden!");
}
record Greeting(String message) {}
}
With quarkus.package.jar.aot.enabled=true and an integration test that exercises this endpoint, Quarkus records the classes and profiles involved in serving it, and the cache carries that forward into every subsequent start.
⚠️ Pro Tip: Training Coverage Is the Whole Ballgame
The cache only reflects the paths your integration tests exercise. If your tests skip the happy path users actually take — no database warm-up, no JSON codec initialisation, no TLS handshake — the cache is thin and the cold-start numbers disappoint. Run integration tests against the full stack: database, caches, external services or their mocks, the whole chain. Thin tests produce a thin cache.
What This Looks Like on Kubernetes
Take a fleet of 50 pods serving steady traffic. Demand spikes 5× and the HPA scales to 250.
Without Leyden: 200 new pods cold-boot, each taking 370 ms (or ~3 s for the CRUD service) before readiness. Until then, the 50 warm pods absorb a 5× spike and risk saturation. Users on the tail of the spike see elevated latency.
With Leyden: the same 200 pods are ready in 80 ms (or ~900 ms). The window of asymmetric load shrinks, warm pods have less time to saturate, and new capacity arrives while it still matters.
The same arithmetic applies to a rolling deployment: ten waves of ten pods each, and every wave’s cold start is a brief dip in capacity. Leyden makes each dip shorter and shallower.
Be honest about the magnitude, though. On Kubernetes, image pull, scheduling and readiness-probe intervals usually dominate pod startup, so 290 ms off a trivial service can vanish into the noise. The compelling case is the heavyweight one — a CRUD service dropping from about three seconds to under one changes how quickly a scale-out event actually delivers capacity, and lets you tighten initialDelaySeconds on the readiness probe, which is often the real bottleneck.
Operational Realities Before You Roll This Out
The benchmarks are the easy part. These are the constraints that decide whether Leyden survives contact with your platform:
- The cache is not portable. It is bound to the exact JDK build, the OS and CPU architecture, the classpath, and — before JDK 26 — the garbage collector used during training. Every JDK patch means regenerating it. Treat cache generation as a build step that runs on every dependency bump, never as an artifact you check in.
- It works against Docker layer reuse. The cache changes on essentially every build, so the layer containing it is never reused. When you count image-size cost, count registry bandwidth and pull time on every deploy, not just megabytes at rest.
- Multi-arch means multiple caches. An arm64 node pool and an amd64 node pool need separately trained caches. If you build multi-platform images, the training run has to happen per platform.
- Silence is the failure mode. A rejected cache does not error; it falls back. Wire
-Xlog:aotoutput into your startup logs and alert on the absence of a successful mapping, or you will discover a stale cache only when someone asks why cold starts got slow again.
⚠️ If you run WebLogic or WebSphere, this is not your story yet
Leyden needs JDK 25 or later. The traditional application servers still certify on JDK 17 and 21, so for app-server estates this remains a roadmap item rather than something to pilot this quarter. Today Leyden is a Quarkus and Spring Boot on Kubernetes story — which is exactly the direction most of those estates are migrating toward anyway. See WebLogic to Kubernetes: a UPI-scale migration playbook for how that path usually runs.
Current Limitations and Roadmap
Leyden is production-ready in Quarkus 3.32+, but the JEP series isn’t finished.
What Has Shipped
- JDK 24: AOT Class Loading & Linking (JEP 483)
- JDK 25: AOT Command-Line Ergonomics (JEP 514) and AOT Method Profiling (JEP 515)
- JDK 26: AOT Object Caching with Any GC (JEP 516) — ZGC support
What’s Next
The Quarkus team highlights two upcoming wins:
- Storing JIT-compiled code in the AOT cache — capturing compiled machine code, not just class metadata and profiles, which would cut warmup further.
- Support for custom class loaders — which would let Quarkus return to
fast-jarmode while still caching startup.
Both are expected in JDK 27 and beyond. If you run ZGC in production, JDK 26 — not 25 — is your entry point, because JEP 516 is what makes the cache usable with it.
💡 Why This Matters for Enterprise Java
For two decades the JVM’s startup story was: accept it, buy faster hardware, or keep pods warm. Leyden changes that without asking you to give up the JVM. The runtime can now carry forward what it learned about your application — which classes to load, which methods matter — and skip rediscovering it on every start. Java gets meaningfully closer to compiled languages on cold-start metrics while keeping the dynamism, tooling and ecosystem that made the JVM worth standardising on.
Conclusion: The New Java Startup Story
Leyden is not the answer to every Java cold-start problem. If you need sub-20 ms boot times, native is still the call. If image size is critical, GraalVM’s 155 MB binary beats the JVM’s 495 MB. If your pods live for days without restarting, cold start barely matters. And if you are on JDK 17 or 21, this is a planning item, not a project.
But for most cloud-native Java workloads — microservices on Kubernetes, containerised APIs, multi-instance deployments — Leyden offers a pragmatic middle ground: roughly 78% faster startup on small services and a three-seconds-to-under-one improvement on large ones, with unchanged peak throughput, no code changes and the full JVM toolchain intact.
It ships in Quarkus 3.32+ and needs one config flag plus an integration-test suite worth training on. Budget for the operational details — cache regeneration on every JDK bump, the lost Docker layer, per-architecture training, and -XX:AOTMode=on in staging so a broken cache fails loudly — and it is a low-risk win for any platform team carrying cold-start pain.
If you’re running Java on Kubernetes, Leyden is worth a test run on your next staging sprint.
Frequently Asked Questions
Is Project Leyden available now, or is it experimental?
Quarkus added Leyden integration in 3.32 (March 2026) and it requires JDK 25 or later. The underlying JEPs shipped progressively: JEP 483 in JDK 24, JEPs 514 and 515 in JDK 25, and JEP 516 in JDK 26. It is a supported feature, but the project itself is still actively evolving.
How much code do I need to change to use Project Leyden?
Zero. Leyden requires one configuration flag (quarkus.package.jar.aot.enabled=true) and running your existing integration tests. No application code changes, no recompilation, no dependency updates beyond JDK 25+.
Does Project Leyden sacrifice runtime performance for faster startup?
No. Peak throughput is unchanged because the JIT is still there — Leyden caches loaded classes, linkage and method profiles, not compiled machine code. Those cached profiles let the JIT compile hot methods immediately, which shortens warmup rather than eliminating it. Native image, by contrast, removes the JIT entirely and can give up some peak throughput on CPU-bound work.
Is 80 ms vs. 370 ms really worth the extra image size?
It depends on the size of your application. On a trivial service, 290 ms often disappears behind image pull and readiness-probe intervals. On a large CRUD service going from around three seconds to under one, the difference is material — that is the case worth optimising for. Note also that the cache layer changes on every build, so it does not benefit from Docker layer reuse.
Should I use Project Leyden or GraalVM native?
Choose Leyden if you want unchanged peak throughput, the full JVM toolchain (JFR, heap dumps, standard profilers) and no reflection configuration to maintain. Choose native image if you need sub-20 ms startup or a minimal memory footprint and can live with a closed-world model. They are points on a spectrum, not competitors.
Further Reading
- Quarkus: “How we integrated Project Leyden into Quarkus” (5 Mar 2026) — The official numbers and integration guide.
- Quarkus 3.32 release announcement — Where the 3 s → 900 ms CRUD figure is stated.
- OpenJDK Project Leyden — The upstream JEP tracker and roadmap.
- JEP 514, JEP 515, JEP 516 — The JDK 25/26 specifications behind the flags used above.
- Spring Boot on Project Leyden: The AOT Cache Recipe That Actually Works — The companion post for Spring Boot estates.
- Quarkus Native Reference Guide — For comparison with GraalVM native if you choose that path instead.
