GraalVM Native Image vs Project Leyden: Two Answers to the Same Cold-Start Problem
Leyden’s AOT approach is fundamentally different from GraalVM’s closed-world assumption. Here is what architects actually need to know to choose the right path — and why the answer is probably not either/or.
If you have ever watched a Java pod spin up in Kubernetes — that long, expensive pause before it is actually ready to serve traffic — you have met the cold-start problem firsthand. For years, the only real answer the Java ecosystem offered was GraalVM Native Image: compile everything ahead of time, ship a binary with no JVM, start in under 100 ms. It worked brilliantly for the right workloads. However, for a large portion of enterprise Java, it also introduced a list of compatibility headaches that teams quietly dreaded.
Now there is a second answer. Project Leyden, an OpenJDK initiative that began shipping real features in JDK 24, takes a fundamentally different approach to the same problem. And because the two solutions look superficially similar — both involve doing work before runtime — architects are understandably confused about which path to take, whether these two approaches conflict, and whether one will make the other obsolete.
This article cuts through that confusion. We will look at what each approach actually does at a technical level, where the real trade-offs sit, and how to match the right tool to your specific deployment context.
1. Why cold start is an architecture problem, not just a performance metric
Startup time matters differently depending on where you sit. For a long-running monolith that restarts once a month, a four-second startup is a footnote. However, for a modern deployment landscape, it frequently determines whether Java is even a viable choice:
| Deployment context | Cold-start impact | Target startup |
|---|---|---|
| Serverless / FaaS | Every cold invocation is billed and user-facing; p99 latency directly suffers | <100 ms |
| Kubernetes pod scale-out | Pods marked ready before JIT warms up; requests hit unoptimized paths for first 10–30 s | <500 ms |
| CLI tooling | Users perceive anything over ~200 ms as “slow to start” | <200 ms |
| Long-running microservices | Startup latency matters less; peak throughput and memory footprint dominate | 1–5 s acceptable |
| Batch / scheduled jobs | Startup overhead is amortized over run duration; rarely the bottleneck | 5+ s often fine |
The important insight here is that the target startup time varies by roughly two orders of magnitude across these contexts. Consequently, a solution that is perfect for serverless is overkill — or even counterproductive — for a long-running service. Both GraalVM and Leyden understand this; they just optimise for different points on that spectrum.
2. GraalVM Native Image: the closed-world bargain
GraalVM Native Image has been in production use since around 2019 and is now a mature, well-understood technology. The idea is elegant: at build time, the native-image tool performs a deep static analysis — called points-to analysis — starting from your application’s main entry point. It traces every reachable class, method, and resource, and compiles the whole lot into a self-contained native binary. At runtime, there is no JVM to initialise, no bytecode to compile, and no class loading delay. The binary starts instantly.
“Native Image is an optimization that reduces the memory footprint and startup time of an application. This requires a closed-world assumption, where all code is known at image build time — no new code is loaded at run time.”
— GraalVM Native Image Compatibility Guide
The performance results are dramatic. A Spring Boot microservice that takes three to four seconds to start on the JVM routinely boots in under 100 milliseconds as a native image, often with 75% lower RSS memory at runtime. For serverless and rapid scale-out scenarios, those numbers are genuinely transformative.
2.1 The cost of the closed-world assumption
However, “all code is known at build time” is a strong constraint, and it creates a very real class of compatibility problems. Because GraalVM cannot know about anything discovered dynamically at runtime — reflection targets, resources, serialization classes, dynamic proxies — you must either eliminate those patterns or hand-write metadata configuration files that tell the tool what it cannot discover on its own.
Reflection, serialization, dynamic proxies, and runtime class loading all require explicit configuration or they simply disappear from the native binary. Furthermore, the application classpath is fixed at build time — no lazy class loading, no OSGi, no runtime plugin systems. Spring’s own documentation notes that the closed-world assumption means beans cannot change at runtime and certain conditional configurations are unsupported.
In practice, teams adopting GraalVM Native Image typically budget meaningful engineering time for what practitioners call the “reflection audit” — systematically identifying every dynamic usage in both their own code and their dependencies, and producing the corresponding configuration. For greenfield microservices with a carefully chosen dependency set, this is manageable. For large, mature enterprise applications with deep framework dependencies built up over years, it can be genuinely painful.
Startup time comparison — Spring PetClinic–scale application (real benchmark data from OpenJDK / GraalVM)

3. Project Leyden: the open-world alternative
Project Leyden starts from a different philosophical premise. Rather than requiring you to give up Java’s dynamic capabilities, it asks: what if we could observe what a real application actually does during a representative run, cache that work, and replay it on every subsequent start? In other words, Leyden is built on speculative optimisation — the same foundation that has driven the JVM’s peak performance for decades.
Practically, this means a training run. You run your application once in a mode that records what it does — which classes it loads, which methods it calls most frequently, what objects it constructs during startup. That profile is stored in an AOT cache file. From then on, every production start loads from the cache and avoids repeating work it has already done. Crucially, if a class is not in the cache, the JVM falls back to loading it normally. There is no hard failure, no constraint violation, no compatibility cliff.
The key architectural difference: Leyden makes no closed-world assumption. The AOT cache is a performance hint, not a constraint. Your application remains fully dynamic — reflection, proxies, custom classloaders, and runtime code generation all still work. You simply avoid re-doing the work that was already observed in training.
3.1 The JEP-by-JEP delivery model
Unlike GraalVM, which shipped as a complete, alternative toolchain, Leyden is delivering incrementally — one JEP per JDK release, each addressing a specific category of startup work. This is deliberate: it lets the Java ecosystem adopt each improvement without any migration cost, and it keeps each feature individually reviewable and reversible.
Leyden premain branch — normalised startup time by optimisation layer (lower is better; 1000 = baseline JVM, real data from OpenJDK Leyden repository)

4. The core philosophical difference
It is worth making the philosophical gap between these two approaches explicit, because this is where the architectural confusion most often originates.
GraalVM Native Image says: “Tell us everything your application could ever do, and we will compile it all into the most optimal binary possible.” The compiler needs a complete, closed view of the world at build time. In exchange, you get the absolute minimum startup time and memory footprint. The trade-off is a hard constraint on dynamism — the tool needs to know about reflection, serialisation, and dynamic proxies ahead of time, or those features simply do not exist in the output binary.
Project Leyden says: “Show us what your application actually does in a representative run, and we will pre-compute as much of that work as possible.” The JVM makes speculative optimisations based on observed behaviour, exactly as the JIT compiler has always done — just shifted earlier in time. In exchange, you keep full Java compatibility. The trade-off is that startup improvements are incremental rather than transformational, and the cache must be regenerated if the application changes significantly.
| Dimension | GraalVM Native Image | Project Leyden |
|---|---|---|
| World model | Closed-world: all code known at build time | Open-world: full Java dynamism preserved |
| Runtime | No JVM; self-contained native binary | Standard HotSpot JVM + AOT cache file |
| JIT compiler | None at runtime — AOT only | Full JIT retained; AOT cache accelerates warmup |
| Reflection support | Manual config required | Full, no changes |
| Dynamic proxies | Must be registered | Supported natively |
| Runtime class loading | Not supported | Fully supported |
| Code changes required | Often significant | None (train + deploy) |
| Peak throughput | Can lag JVM for adaptive workloads | Full JIT peak performance |
| Startup improvement | ~40–50× vs baseline JVM | ~2–4× today; more with premain branch |
| Memory (RSS) | ~75% lower than JVM | Modest improvement; JVM overhead remains |
| Container image size | Very small (no JVM needed) | Larger (JVM + cache file) |
| Tooling maturity | Production-ready, rich ecosystem | JDK 24–26 features shipping; premain experimental |
| GraalVM / Oracle dependency | Yes | No — standard OpenJDK |
| Spring Boot support | Yes (AOT processing required) | Yes (3.3+ out of the box) |
5. Choosing the right path for your workload
Rather than treating this as a binary choice, it helps to think of the two approaches as serving different segments of the deployment spectrum. The decision is not primarily about technology preference — it is about workload profile, team capacity, and how much compatibility risk you can absorb.
Lean toward GraalVM Native Image when…
- Startup under 200 ms is a hard requirement (serverless, CLI tools, rapid auto-scaling)
- Memory footprint per instance is a primary constraint (dense container packing)
- Your application is a focused microservice with a well-controlled dependency set
- You can invest engineering time in the reflection audit and compatibility work
- You are building new services, not migrating existing ones
- Peak throughput is less important than startup latency and memory
Lean toward Project Leyden when…
- You need meaningful startup improvement with zero code changes or compatibility risk
- Your app is reflection-heavy (Spring, Hibernate, Quarkus CDI) or uses dynamic proxies heavily
- You are migrating large, mature enterprise applications
- Peak JIT throughput matters and you cannot accept any regression
- Your team uses OpenJDK and wants to avoid a vendor dependency on GraalVM
- You need to support ZGC (fully unlocked as of JDK 26 via JEP 516)
Not a binary choice: Quarkus already supports both paths side by side — plain JVM, Leyden-optimised JVM, and native image are three modes that coexist. As the Quarkus team puts it, “rather than replacing one another, these modes form a spectrum of trade-offs.” The same thinking applies to any modern Java framework.
6. What Leyden is not — and a word on timelines
It is important to be accurate about where Leyden stands today, because the project is sometimes described with more confidence than the current state warrants. As of April 2026, four JEPs have shipped (JDK 24–26), delivering roughly a 40–60% startup improvement for well-configured applications. That is significant, but it is not the same league as GraalVM’s 40–50× improvement for serverless scenarios.
The most exciting Leyden features — ahead-of-time code compilation (pre-compiled native methods) and ahead-of-time dynamic proxy generation — are currently only in the experimental premain branch of the OpenJDK repository, not yet in any released JDK. These are the features that push the premain benchmark down to roughly 25% of baseline startup time. They are real and they work, but they are not production-ready today.
The premain branch benchmark showing startup at 25% of baseline is promising, but it reflects experimental code. Production teams should plan around what is actually shipped: JEP 483 + 514 + 515 (JDK 24–25) for a reliable ~40–50% startup improvement today, with more to come incrementally. Spring Boot 3.3+ supports Leyden’s AOT cache out of the box, and Quarkus has active integration work underway.
7. What we have learned
Throughout this article, we have seen that GraalVM Native Image and Project Leyden are, at their core, two different answers to the same question — not the same answer with different branding. GraalVM solves the cold-start problem by eliminating the JVM entirely and requiring a closed-world view of your application at build time. It delivers transformational startup improvements (sub-100 ms) and dramatic memory savings, at the cost of compatibility constraints and engineering effort. For serverless, CLI tooling, and tightly scoped microservices, those trade-offs are often well worth it.
Project Leyden, by contrast, builds on Java’s open-world model and the JVM’s existing speculative optimisation machinery. It ships incrementally — one JEP per release — and today delivers a solid 40–60% startup reduction with zero code changes and full backward compatibility. It preserves the JIT compiler, reflection, dynamic proxies, and every other dynamic Java capability. For large enterprise applications, migration projects, and teams that cannot afford compatibility risk, it offers a path to meaningfully better startup without any of GraalVM’s constraints.
Furthermore, as Quarkus, Spring, and other frameworks demonstrate, the two approaches are not mutually exclusive. The right architecture increasingly offers both deployment modes, letting teams choose based on workload profile rather than technical ideology. Architects who understand both paths clearly — and resist the temptation to frame this as a competition — will be best positioned as Leyden’s premain features graduate to mainline JDK releases over the next one to two years.



