Diagnosing Quarkus Native Image Build Failures: Symptoms, Checks, and Fixes
A diagnostic guide to Quarkus native-image failures: a symptom-to-cause table, ordered checks from version verification to log analysis, targeted fixes for reflection, resources, memory, and unsupported APIs, plus clear escalation criteria.
12 Dec 2025, 12:54 UTC

Your Quarkus app runs perfectly in dev mode and as a fast-jar, but ./mvnw package -Pnative dies with a wall of GraalVM output, or worse, the binary builds and then throws ClassNotFoundException the first time it handles a request. The useful takeaway: nearly every native-image failure falls into one of five buckets (reflection, resources, JNI, unsupported APIs, or memory), and the build log tells you which bucket you are in if you know which lines to read.
Why native builds fail when JVM builds pass
GraalVM native-image performs closed-world analysis at build time: it must see every class, method, and resource your application can touch before it compiles. Anything reached through reflection, dynamic proxies, JNI, or classpath scanning that the static analysis cannot prove is reachable gets dropped. Dev mode never exercises this analysis, so configuration gaps only surface at package time or, worse, at runtime in the native binary.
Version assumptions for this guide: Quarkus 3.x with GraalVM for JDK 17 (22.3+) or JDK 21 (24.0+). A mismatched GraalVM produces cryptic errors that look like everything except a version problem, so check that first.
Symptom-to-cause table
| Symptom | Likely cause | Diagnostic indicator |
|---|---|---|
ClassNotFoundException at runtime in native binary | Missing reflection registration | Class is loaded reflectively (Jackson, JAXB, custom serialization); works in JVM mode |
UnresolvedElementException during analysis | Class referenced but not on the native classpath, or wrong GraalVM version | Build log names the missing element; often a transitive dependency |
Build killed or OutOfMemoryError from native-image | Insufficient heap for the analysis | CI runner or container memory limit hit; exit code 137 in containers |
UnsupportedFeatureException | API with no native support (e.g., javax.script, some dynamic classloading) | Log names the unsupported feature and the call path |
| Resource missing at runtime (templates, config files, native libs) | Resource not included in the image | File exists in the fast-jar but not in the native binary |
JNI UnsatisfiedLinkError | Native library not registered or not present | Only affects code paths calling into C libraries (compression, crypto) |
Ordered checks, from cheapest to most expensive
- Confirm the GraalVM version. Run
java -version(or checkJAVA_HOME) and compare against the GraalVM/Mandrel version your Quarkus BOM expects. Do this before anything else; version skew wastes more debugging hours than any other cause. - Reproduce with verbose output. Run
./mvnw package -Pnative -Dquarkus.native.additional-build-args=--verbosefrom the project root. You need write access totarget/and enough memory (plan for 4–8 GB free). - Read
target/native-image.log. Search forError:andWarning:lines. Warnings of the formWarning: class initialization of class ... failedorCould not register ... for reflectionusually point directly at the class you need to handle. Do not stop at the Maven summary; the actionable lines are in the log. - Check whether the failing class is yours or a dependency's. Your own classes need one annotation; third-party classes need configuration, and transitive dependencies of indexed libraries are not covered automatically.
- Verify resources. If the binary runs but misses files, check
quarkus.native.resources.includesandexcludesinapplication.properties. - Profile memory if the build dies. Set
quarkus.native.native-image-xmx=6g(adjust to your machine) and retry. In containers, the limit must be below the container's memory cap, not above it. - Test the binary, not just the build. Run
./target/<artifact>-runnerand hit the endpoint or code path that failed. A green build does not prove reflection config is complete.
Fixes tied to each finding
Missing reflection registration
For classes you own, annotate them:
@RegisterForReflection
public class OrderCreatedEvent {
public String orderId;
public Instant createdAt;
}
For third-party classes, create src/main/resources/reflect-config.json (standard GraalVM format) or, for a whole dependency, index it so Quarkus processes its Jandex metadata:
quarkus.index-dependencies.my-lib.group-id=com.example
quarkus.index-dependencies.my-lib.artifact-id=my-library
Limitation: indexing covers the named artifact, not its transitive dependencies. If the error names a class from a transitive jar, register that class explicitly.
Missing resources
quarkus.native.resources.includes=templates/**,schemas/*.xsd
Verify by running the binary and exercising the path that loads the resource; there is no reliable static check.
Out of memory during the build
Raise the heap and consider disabling isolates on constrained CI runners:
quarkus.native.native-image-xmx=6g
quarkus.native.additional-build-args=-H:-SpawnIsolates
Disabling isolates reduces peak memory at some runtime cost for multi-threaded workloads; measure before keeping it in production.
Unsupported API
When the log names an unsupported feature, the fix is replacement, not configuration. The classic example is javax.script: swap it for org.graalvm.polyglot, which is designed for native-image embedding. If the unsupported call is inside a library, check whether that library publishes a Quarkus extension; extensions exist precisely to carry native-image metadata for libraries that cannot be analyzed statically.
Slow or stuck analysis
Initializing a problematic package at build time can move work out of the static analysis:
quarkus.native.additional-build-args=--initialize-at-build-time=com.example.problematic
Apply this narrowly. Broad build-time initialization changes class-initialization order and can introduce new failures.
Local vs. container builds
Behavior can differ between a local GraalVM install and -Dquarkus.native.container-build=true, because the builder image pins its own GraalVM/Mandrel version. If you deploy containers, validate with the container build even when the local build passes, and add a CI step that starts the produced binary inside the target image (even just ./application --help or a health endpoint check) so a bad binary fails the pipeline instead of the deploy.
How to verify the fix actually worked
- Re-run
./mvnw clean package -Pnative -DskipTestswith the same GraalVM CI uses, and confirm the warning lines you targeted are gone fromtarget/native-image.log. - Run the binary and compare startup time and RSS against the fast-jar; a native binary that starts in seconds instead of milliseconds often means something is falling back or re-initializing.
- Exercise the exact code path that failed originally, not just the health endpoint.
When to escalate instead of debugging further
- The build fails identically across two supported GraalVM versions: file a minimal reproducer against Quarkus; you have likely hit a real bug.
- A third-party library has no native support and no extension: open an issue with the library maintainer or evaluate an alternative library rather than hand-maintaining large config files.
- CI build time cannot be brought under budget: keep native builds for release pipelines and use
fast-jarfor per-commit checks. - The team lacks GraalVM expertise and the failure is in analysis internals: engage the Quarkus community (Zulip, GitHub discussions) or vendor support with the full
native-image.logattached.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.