GraalVM Native Image with Spring Boot: When Is It Worth It?
'The application starts in 60 milliseconds' is impressive; what does it cost? We compare GraalVM native image with the JVM and Java 25's AOT cache on startup, memory, peak throughput and compatibility; which scenarios really justify it, the step-by-step build with Spring Boot, RuntimeHints and the tracing agent, pitfalls such as profiles fixed at build time, and a decision guide.
"The Spring Boot application starts in 60 milliseconds and runs on 80 MB of memory." This sentence from GraalVM native image talks is true and impressive. The part not said in the talk is also true: the build takes 5–10 minutes and 8 GB of RAM, every library that uses reflection needs metadata, peak throughput stays behind the JIT-powered JVM, and some decisions such as profiles are fixed at build time. Native image isn't magic, it's a trade; for enterprise teams the question isn't "can we do it" but "for which service is it worth it".
This article covers what native image does, the comparison with the JVM and Java 25's AOT cache, the scenarios where it pays off and where it doesn't, the step-by-step build with Spring Boot, RuntimeHints and the tracing agent, the problems met in the field and a decision guide.
What does native image do?
GraalVM native image analyzes the application and its dependencies at build time (the "closed world" assumption: no new classes are loaded at runtime), compiles everything reachable to machine code and produces a single executable that runs without a JVM. Class loading, bytecode verification and JIT warm-up disappear; startup drops to milliseconds, memory shrinks. The price is that dynamic behavior (reflection, dynamic proxies, resource loading, serialization) must be declared at build time. Spring Boot 3 resolves bean definitions at build time with the Spring AOT engine and generates most of the metadata the framework needs automatically; the rest is the responsibility of the application and third-party libraries.
Three options side by side
Criterion | JVM (JIT) | JVM + CDS / AOT cache (Java 25) | GraalVM native image |
|---|---|---|---|
Startup (mid-size Spring Boot service) | 8–20 s | 3–8 s | 0.05–0.3 s |
Memory (RSS, idle) | 300–600 MB | 250–500 MB | 60–150 MB |
Peak throughput | Highest (profile-guided JIT optimization) | Same | 10–40% lower (PGO closes the gap; Oracle GraalVM) |
Warm-up | Minutes | Minutes | None; the first request at full speed |
Build time / resources | Seconds | Seconds + a training run | 3–15 min, 6–12 GB RAM |
Compatibility | Full | Full | Closed world: metadata for reflection/proxies/resources; some libraries unsupported |
Observability | JFR, profilers, agents, heap dumps | Same | Limited JFR, no agents; Micrometer/OTel work |
GC | G1/ZGC/Shenandoah | Same | Serial (Community), G1 (Oracle GraalVM) |
Debugging | Easy | Easy | Hard; failures at build time or "class not found" at runtime |
The critical point: with the AOT cache (Leyden) in Java 24–25, JVM startup has already shortened noticeably. For most enterprise services "10 seconds → 4 seconds" is enough improvement and doesn't require paying native image's compatibility price (see Java 25).
When it pays off, and when it doesn't
Worth it | Not worth it |
|---|---|
Scale-to-zero environments: AWS Lambda, Cloud Run, Knative — every cold start is user-facing latency | Long-running, high-throughput services: the JIT wins at peak, startup happens once a day |
Many small services with low traffic: memory rent drops, pod density rises | Applications heavy on reflection/dynamic libraries (old ORMs, script engines, some SAP/JNI bridges) |
CLI tools and short-lived jobs: the user waits for startup | Teams with limited CI capacity: every build costs minutes and gigabytes |
Edge/IoT and constrained memory | Operations that depend on deep profiling and agents (APM agents, heap analysis) |
Services needing fast horizontal scaling and instant rollback | Large monoliths with a high rate of change and many third-party dependencies |
Step by step with Spring Boot
<!-- pom.xml: the Spring Boot 3.x parent already includes AOT support; the native profile adds the GraalVM plugin -->
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles># 1) Local build with a GraalVM JDK (Oracle GraalVM or GraalVM Community / Liberica NIK)
./mvnw -Pnative native:compile
./target/order-service # up in 60–200 ms
# 2) A container image with Buildpacks, without installing GraalVM
./mvnw -Pnative spring-boot:build-image # BP_NATIVE_IMAGE=true; image ~100 MB, no JVM required
# 3) Native tests: the JUnit tests run inside the native executable
./mvnw -PnativeTest testHints for reflection and resources
// Types serialized with Jackson that Spring can't see (e.g. DTOs only loaded dynamically)
@RegisterReflectionForBinding({InvoiceDto.class, SapReturnRecord.class})
@Configuration
class SerializationConfig {}
// More general: reflection, resource and proxy registrations through a RuntimeHintsRegistrar
@ImportRuntimeHints(IntegrationHints.class)
@Configuration
class IntegrationConfig {}
class IntegrationHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader cl) {
hints.reflection().registerType(LegacyClient.class, MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.DECLARED_FIELDS);
hints.resources().registerPattern("templates/*.xml");
hints.proxies().registerJdkProxy(OrderPort.class);
hints.serialization().registerType(TypeReference.of("com.acme.legacy.LegacyRecord"));
}
}Instead of finding by hand what each library needs, use the tracing agent: the application runs on the JVM with the agent, tests and typical flows are exercised, and the agent writes reflection/resource/proxy usage as JSON under META-INF/native-image/. GraalVM's reachability metadata repository provides ready-made metadata for common libraries; Spring's own hints come automatically. The registrations above cover the remaining gaps.
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/acme/order-service \
-jar target/order-service.jar # then trigger all flows and the integration testsProblems met in the field
Profiles and conditions are fixed at build time. Spring AOT decides
@Profileand@Conditionalduring the build; an image built with theprodprofile can't load different beans under thetestprofile. Configuration values that vary by environment (URLs, secrets) are read at runtime; but the bean set is determined at build time. You need an image per environment or a profile-independent design."Class not found" at runtime: Reflection without metadata. Caught before the build with the tracing agent and native tests; every one caught in production means a release.
Unsupported libraries: JNI-based bridges (such as SAP JCo), some script engines, old dynamic-proxy libraries. A service with such a dependency stays on the JVM.
The memory illusion: RSS drops but the heap requirement stays the same;
-Xmxis still set. The Community edition has Serial GC; pauses grow at high allocation rates.Build infrastructure: 8–12 GB of RAM on CI runners; build time of minutes per PR. Native builds usually only for the main branch and releases, JVM tests on PRs.
The observability gap: APM agents don't work; the OpenTelemetry SDK and Micrometer do. Not adopted before the operations team's tools are checked (see observability).
The peak-throughput surprise: The JVM coming out 20% ahead in a load test is normal; PGO (profile-guided optimization) on Oracle GraalVM closes the gap but needs an extra training step.
Decision guide
Does the service scale to zero, or does cold start affect the user experience? If not, try the Java 25 AOT cache first; it's probably enough.
Is the dependency list native-compatible? A one-day trial with the tracing agent and native tests answers that.
Can the operations tools (profiling, agents, heap dumps) be covered on native?
Are the CI build capacity and duration acceptable?
Is peak throughput critical? If yes, the JVM; or Oracle GraalVM + a PGO measurement.
In practice the typical enterprise landscape is mixed: functions, CLI tools and rarely called small services native; core transactional services JVM + AOT cache. The same codebase can be built both ways; once native compatibility is achieved, the choice is made per service.
Frequently asked questions
Is CRaC an alternative?
Yes, with a different trade: CRaC (Coordinated Restore at Checkpoint) takes a snapshot of the warmed-up JVM and restores it in under a second; full JVM compatibility is preserved and the JIT gains remain. The price: Linux-only, checkpoint discipline (closing open connections and files; supported by Spring Boot 3.2+), and the snapshot may contain secrets. If compatibility is a problem but startup is critical, try CRaC before native.
Oracle GraalVM or Community?
Community (and distributions such as Liberica NIK) is free and enough for most scenarios; Oracle GraalVM offers G1 GC, PGO and better peak performance, with license terms (GFTC) to be evaluated for production use.
Does native image make a difference for security?
The attack surface shrinks (no JVM or class loading, closed world), but dependency vulnerabilities are compiled in as they are; SBOM and scanning are still needed. The executable can't be patched without a rebuild; the patch process is an image rebuild.
Conclusion
GraalVM native image is an extraordinary tool in the right place: scale-to-zero services, functions, CLI tools and dense clusters of small services. In the wrong place — high-throughput core services, reflection-heavy dependencies, limited CI — its cost exceeds its gain. Java 25's AOT cache delivers "fast enough startup" for most enterprise services without native; the native decision is made by measurement, per service, after a one-day trial with the tracing agent.
To measure your services' startup and memory profile, evaluate native compatibility or move the right services to native, see our software design and architecture service.