Cutting Quarkus Startup Time with GraalVM Native Images
Want to shrink Quarkus startup time and memory usage? Enable native images with GraalVM. This guide walks through the one‑liner build change, a minimal REST example, and the trade‑offs you’ll face.
15 May 2026, 16:59 UTC

Why native images matter for Quarkus
Quarkus is built for containers, but the JVM still adds a 2–5 second warm‑up and a 200–400 MB memory footprint. For serverless or micro‑service workloads that scale up and down frequently, that overhead can dominate cost and latency. GraalVM’s native image feature turns a Quarkus application into a single executable that starts in milliseconds and uses only a few tens of megabytes of RAM.
Enabling native mode – the one‑liner
Quarkus hides the complexity behind a simple flag. In a Maven project add the property in pom.xml:
quarkus.native.enabled
true
or invoke it from the command line:
./mvnw package -Pnative -Dquarkus.native.enabled=true
For Gradle, set quarkus.native.enabled = true in build.gradle.kts. The flag tells the Quarkus Maven/Gradle plugin to invoke GraalVM’s native-image compiler during the build.
Minimal REST example – build and run
Below is a bare‑bones Quarkus 3.x project that exposes a single endpoint. The example demonstrates the full flow from source to native binary.
- Project structure – only
src/main/java/org/acme/GreetingResource.javaandpom.xmlare needed. - GreetingResource:
package org.acme;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, Quarkus native!";
}
}
- pom.xml snippet – minimal dependencies and native flag:
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
<version>3.9.0.Final</version>
</dependency>
</dependencies>
<properties>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
- Build – run:
./mvnw clean package -Pnative
Prerequisites: GRAALVM_HOME must point to a GraalVM 22.x+ installation that matches the Java version Quarkus uses (Java 17 or 21). Set JAVA_HOME to $GRAALVM_HOME before invoking Maven. The build will download the GraalVM binaries if not already present.
- Run the native binary:
./target/quarkus-app/quarkus-run.native
Access http://localhost:8080/hello – you should see the string “Hello, Quarkus native!”. Check the startup time with time and memory usage with ps -o rss or top -b -n 1 | grep quarkus-run.native. The native image typically boots in <1 ms and stays under 50 MB RAM, compared to 2–5 s and 300 MB for the JVM variant.
What happens under the hood
The native image compiler performs:
- Tree‑shaking – removes unused classes and methods.
- Reflection analysis – pre‑computes metadata for classes accessed via reflection. Quarkus automatically generates most of this, but libraries that rely on runtime classloading may need explicit
reflect-config.jsonentries. - Native runtime – the resulting binary contains a minimal Java runtime and the compiled bytecode, eliminating the JVM startup overhead.
Trade‑offs and limitations
- Build time – native compilation can take 3–10 minutes for large projects. Every code change triggers a full rebuild; incremental updates are not supported.
- Dynamic features – Java features that rely on runtime code generation (e.g., dynamic proxies, bytecode manipulation libraries) may not work. Quarkus provides native‑compatible alternatives (e.g.,
quarkus-resteasy-jacksonuses Jackson’s native APIs). - Library support – some third‑party libraries have no native‑image configuration. If you encounter
UnsupportedOperationExceptionat runtime, consult the library’s documentation or add areflect-config.jsonentry. - Size – while native images use less memory, the binary can still be large (hundreds of MB) if many dependencies are pulled in. Use
quarkus.native.additional-build-args=-H:IncludeResources=…to strip unused resources.
How to verify the build and runtime
After building, run a quick check to ensure the binary is functional:
./target/quarkus-app/quarkus-run.native &
PID=$!
# Wait for server to start
sleep 2
curl -s http://localhost:8080/hello
kill $PID
Verify memory usage with:
ps -o rss,cmd -p $PID | grep quarkus-run.native
Compare the output to the JVM version:
time java -jar target/quarkus-app/quarkus-run.jar
ps -o rss,cmd -p $!
Typical differences: startup <1 ms vs 3 s; memory <50 MB vs 300 MB.
Next steps for production
- Use
-Dquarkus.native.container-build=trueto build the native image inside a Docker container, ensuring reproducibility. - Deploy the binary to lightweight container runtimes (e.g.,
distroless) to further reduce image size. - Enable continuous integration to automatically rebuild the native image on each release.
- Monitor startup and memory metrics in production – Quarkus exposes
/q/metricswhich can be scraped by Prometheus.
Native images are a powerful tool for Quarkus developers who need ultra‑fast startup and low memory consumption. With the one‑liner flag and a clear understanding of the trade‑offs, you can quickly evaluate whether native mode fits your workload.
Conclusion
Quarkus’ GraalVM native image feature turns a Java microservice into a near‑instant startup, low‑memory executable. Enabling it is trivial, but you must plan for longer build times, library compatibility, and potential runtime adjustments. Use the example above to get started, then iterate on reflection configuration and resource pruning to squeeze the binary size further.
Diagram
| Native Image Build Flow |
|---|
| Source → GraalVM Compiler → Binary |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.