Quarkus has quickly become one of the most popular JVM frameworks for building cloud-native, container-friendly applications. Its core philosophy—“Supersonic Subatomic Java”—comes from two key performance goals:
-
Blazing-fast startup time, ideal for serverless, FaaS, and container scaling.
-
Low memory footprint, making it highly efficient on Kubernetes, microservices, and edge environments.
Unlike traditional Java frameworks that rely heavily on runtime classpath scanning, reflection, and dynamic configuration, Quarkus pushes as much work as possible to build time. This build-time optimizations model makes Quarkus extremely fast in both JVM mode and, especially, when compiled to a native executable using GraalVM or Mandrel.
However, like any framework, Quarkus applications don’t automatically reach peak performance without the right configuration. Startup time, live memory consumption, and overall runtime efficiency can vary depending on:
-
The extensions you include
-
Your build parameters
-
Database connection settings
-
JVM or native configurations
-
How much reflection or dynamic loading does your code use
What You Will Learn
In this tutorial, we will walk through the most practical techniques to optimize Quarkus performance. You will learn how to:
-
Measure baseline performance (startup time, memory usage, throughput)
-
Improve startup time in JVM mode
-
Reduce RAM usage in both JVM and native builds
-
Create optimized native executables with GraalVM or Mandrel
-
Tune your application.properties for maximum efficiency
-
Optimize database connections and caching
-
Profile and monitor your application with JDK Flight Recorder and Micrometer
-
Apply real-world improvements to a sample Quarkus REST API
By the end, you’ll be able to build highly efficient, production-ready Quarkus applications that scale faster, consume fewer resources, and perform better under load.
Project Setup
To begin optimizing Quarkus, we need a baseline project to measure and improve. In this section, we’ll create a simple REST API using the Quarkus CLI, add the essential extensions, and prepare our development environment for JVM and native builds.
1. Install Quarkus CLI (If Not Installed)
If you don’t have the Quarkus CLI, install it using SDKMAN (recommended):
sdk install quarkus
Or upgrade to the latest:
sdk upgrade quarkus
You can verify the installation with:
quarkus --version
The CLI helps generate projects, add extensions, build native images, and run dev mode with improved speed.
2. Create a New Quarkus Project
Use the Quarkus CLI to scaffold a project with RESTEasy Reactive:
quarkus create app com.djamware:quarkus-performance:1.0.0 \
--extensions="resteasy-reactive,resteasy-reactive-jackson"
This generates the following structure:
quarkus-performance/
├── src/
├── pom.xml
├── README.md
└── src/main/java/com/djamware
Why RESTEasy Reactive?
-
Faster startup
-
Lower memory usage
-
Non-blocking I/O optimized for modern workloads
-
Preferred over traditional RESTEasy Classic
3. Add Optional Extensions for This Tutorial
We will need additional extensions later in the tutorial for performance testing, caching, and metrics.
Add them now:
cd quarkus-performance
quarkus extension add \
quarkus-smallrye-health \
quarkus-micrometer-registry-prometheus \
quarkus-hibernate-orm-panache \
quarkus-jdbc-h2 \
quarkus-cache
Extensions Breakdown
| Extension | Purpose |
|---|---|
| Health Check | Monitor readiness & liveness |
| Micrometer + Prometheus | Metrics for startup/memory profiles |
| Hibernate + Panache | Simple database layer for real-world testing |
| H2 Database | In-memory DB for benchmarking |
| Quarkus Cache | Memory optimization example |
4. Create a Simple REST Endpoint
Let’s add a basic API we’ll use as our baseline.
Create:
src/main/java/com/djamware/resource/HelloResource.java
package com.djamware.resource;
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 HelloResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public String hello() {
return "{\"message\":\"Hello from Quarkus!\"}";
}
}
Start the application in dev mode:
quarkus dev
Then open:
http://localhost:8080/hello
If you see the JSON response, your baseline API is ready.
5. JVM vs Native Profiles
Quarkus has three important modes:
| Mode | Command | Use Case |
|---|---|---|
| Dev Mode | quarkus dev |
Live reload, fast development |
| JVM Build | mvn package |
Production JVM deployment |
| Native Build | mvn -Pnative package |
Ultra-fast startup, low memory footprint |
We’ll use all three throughout this tutorial.
6. Prepare Application Configuration
Open src/main/resources/application.properties and add:
quarkus.http.port=8080
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
# Enable metrics
quarkus.micrometer.export.prometheus.enabled=true
# Hibernate optimization
quarkus.hibernate-orm.sql-load-script=no-file
This gives us a reasonable baseline for startup time and memory measurements.
Your project is now fully set up.
Measuring Baseline Performance
Before we optimize anything, we need to understand how the application behaves as-is. Establishing a baseline allows us to quantify improvements in startup time, memory consumption, and runtime performance later in the tutorial.
In this section, you’ll learn how to measure:
-
JVM startup time
-
Native startup time (later, after we generate a native build)
-
Memory usage while running
-
API throughput using simple load testing tools
1. Measure JVM Startup Time
Build the application in JVM mode:
./mvnw clean package
Then run the JAR manually:
java -jar target/quarkus-app/quarkus-run.jar
When Quarkus starts, you’ll see logs like:
INFO ... started in 0.997s. Listening on: http://0.0.0.0:8080
Record this value—it is your baseline startup time in JVM mode.
2. Measure Memory Usage (JVM)
With the app still running, open another terminal and run:
ps -o pid,rss,command | grep quarkus-run
Example output:
49791 146176 java -jar target/quarkus-app/quarkus-run.jar
50281 1296 grep quarkus-run
Here:
-
RSS = 146176 KB (≈ 140 MB)
-
This is the baseline live memory usage.
Alternatively, you can watch usage over time:
top -p <PID>
or use JDK tools:
jcmd <PID> VM.native_memory summary
JVM memory usage depends on your machine, Java version, and OS. Your numbers may vary slightly.
3. Measure Endpoint Performance
We will test the /hello endpoint using curl and wrk (or hey).
Quick test with curl
curl -w "Total: %{time_total}s\n" http://localhost:8080/hello
Throughput test with wrk
If you have wrk installed:
wrk -t4 -c50 -d10s http://localhost:8080/hello
Example output:
Requests/sec: 19000
Latency: 2.1ms
Or with hey (Go-based HTTP load tester):
hey -n 5000 -c 50 http://localhost:8080/hello
Record:
-
Requests per second
-
Average latency
-
Maximum latency
These will be useful later.
4. Accessing Metrics (Micrometer + Prometheus)
Since we installed Micrometer, you can check runtime metrics at:
http://localhost:8080/q/metrics
Useful metrics include:
-
process_start_time_seconds -
jvm_memory_used_bytes -
http_server_requests_seconds_*
These metrics help validate the accuracy of your manual measurements.
5. Summary: Baseline Results Table
Create a table to capture your baseline. Example:
| Metric | JVM Mode (Baseline) |
|---|---|
| Startup Time | ~1.0s |
| Memory Usage (RSS) | ~140 MB |
| Hello Endpoint Throughput | ~18k–20k req/sec |
| Avg Latency | ~2 ms |
Your exact numbers may differ, but the goal is to establish your own baseline.
We will revisit these metrics after:
-
Extension optimization
-
Startup time improvements
-
Database tuning
-
Native image compilation
So you can clearly see the performance gains.
Reduce Startup Time (JVM Mode)
Quarkus is already fast by default, but there are several proven techniques to reduce startup time even further. Most improvements come from reducing reflection, trimming unused extensions, and shifting more work to build-time initialization.
This section covers the most impactful optimizations you can apply in JVM mode before moving to native builds.
1. Remove Unused Extensions
Extensions directly impact startup time—each extension adds additional initialization steps.
List installed extensions:
quarkus extension list
Or list only those in your project:
quarkus info
If you see any extensions not needed for your API, remove them:
quarkus extension remove quarkus-xyz
Fewer extensions = less startup overhead.
2. Use RESTEasy Reactive Instead of Classic
If you installed RESTEasy Classic previously, replace it with RESTEasy Reactive (which we already did in Project Setup).
It provides:
-
Faster startup
-
Lower memory footprint
-
Netty-based non-blocking I/O
To confirm you're using Reactive:
quarkus info
Look for:
quarkus-resteasy-reactive
If “resteasy” (classic) appears, remove it.
3. Disable Unused Features in application.properties
Disable automatic features you are not using:
quarkus.banner.enabled=false
quarkus.hibernate-orm.sql-load-script=no-file
quarkus.arc.remove-unused-beans=true
quarkus.jackson.fail-on-unknown-properties=false
What these do:
-
remove-unused-beans: removes unused CDI beans at build time -
hibernate no-file: avoids startup delay caused by checking SQL load scripts -
banner=false: small improvement, but reduces noise
4. Move Initialization to Build-Time (Avoid Reflection)
Quarkus encourages build-time initialization.
Use constructor injection instead of field injection
Bad (requires reflection):
@Inject
MyService service;
Good (no reflection):
@ApplicationScoped
public class HelloService {
private final Repository repo;
public HelloService(Repository repo) {
this.repo = repo;
}
}
Benefits:
✔ Faster startup
✔ Fewer reflective lookups
✔ Better native build compatibility
5. Reduce Classpath Scanning
Set the following:
quarkus.arc.detect-unused-beans=all
This aggressively removes unused CDI beans.
You can also restrict JPA entity scanning:
quarkus.hibernate-orm.packages=com.djamware.entity
6. Enable Classpath Cache (Big Win)
Quarkus can cache the resolved classpath for faster builds and dev mode startup:
quarkus.bootstrap.classpath-cache=true
The cache is stored in .quarkus/ and reused automatically.
Benefits:
-
Faster
quarkus dev -
Faster incremental rebuilds
-
Shorter JVM startup during packaging
7. Use Fast JAR Packaging
Ensure your JAR is built in “fast-jar” mode:
quarkus.package.jar.type=fast-jar
This is the default, but adding it explicitly helps ensure optimal startup.
8. Use Continuous Testing to Reduce Cold Starts
Instead of repeatedly running fresh builds for tests:
quarkus test
Or start dev mode and press:
r
Continuous testing avoids reboots and shortens the developer cycle startup time.
9. Example: Before vs After Startup Time
Here’s an example of typical improvements from the settings in this section:
| Optimization Step | Startup Time |
|---|---|
| Baseline JVM Startup | ~1.0s |
| Remove unused extensions | ~0.95s |
| Enable build-time bean removal | ~0.85s |
| Classpath cache enabled | ~0.75s |
| RESTEasy Reactive + config tweaks | ~0.70s |
On a typical machine, these optimizations reduce startup time by 25–40%.
Your numbers may differ, but similar gains are common.
Your Quarkus application is now much faster in JVM mode!
Improve Memory Consumption
Quarkus significantly reduces memory usage compared to traditional Java frameworks by pushing work to build time, minimizing reflection, and optimizing CDI. However, there are still several ways to further decrease heap and native memory usage — especially important for Kubernetes, serverless, small containers, and cost-efficient deployments.
This section covers the most effective techniques to reduce memory consumption in JVM mode and prepare for even bigger savings in native mode.
1. Use Properly Sized Database Connection Pools
Database connection pools are one of the biggest sources of memory use in microservices.
Default pool sizes are often larger than needed.
Reduce them in application.properties:
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
quarkus.datasource.jdbc.initial-size=2
For small services, you can go even lower:
quarkus.datasource.jdbc.max-size=4
Why this matters:
-
Each connection consumes RAM
-
Lower pool sizes = lower memory footprint
-
Great for low-concurrency or read-heavy services
2. Prefer Reactive SQL Clients (If Applicable)
If your service doesn’t require Hibernate ORM or JDBC, use the reactive client:
quarkus extension add quarkus-reactive-pg-client
Benefits:
✔ Lower memory usage
✔ No heavyweight JDBC driver
✔ Non-blocking I/O reduces thread usage
3. Reduce Hibernate ORM Memory Usage
If you use JPA, apply these settings:
# Disable 2nd-level cache unless needed
quarkus.hibernate-orm.second-level-caching-enabled=false
# Avoid unnecessary metadata generation
quarkus.hibernate-orm.sql-load-script=no-file
# Disable statistics
quarkus.hibernate-orm.statistics=false
If your app uses only a few entities, explicitly define scan packages:
quarkus.hibernate-orm.packages=com.djamware.entity
This prevents scanning the entire classpath.
4. Disable Unused CDI Beans
Quarkus aggressively removes unused CDI beans, but you can push further:
quarkus.arc.remove-unused-beans=true
quarkus.arc.detect-unused-beans=all
Results:
-
Fewer beans are loaded into memory
-
Lower startup overhead
-
Smaller runtime footprint
5. Use Caching for Expensive Calls
The Quarkus Cache extension reduces CPU and memory churn by memoizing results.
Example:
@CacheResult(cacheName = "hello-cache")
public String getHello() {
return "Hello " + System.currentTimeMillis();
}
Configure cache size:
quarkus.cache.caffeine.hello-cache.initial-capacity=16
quarkus.cache.caffeine.hello-cache.maximum-size=100
Benefits:
-
Prevents recalculating heavy operations
-
Reduces transient object allocation
-
Smooths memory spikes under load
6. Tune JVM for Container Environments
When running in Docker or Kubernetes, the JVM needs explicit memory boundaries.
Enable container support automatically:
# JVM respects container memory limits
-XX:+UseContainerSupport
(This is enabled by default in modern Java versions.)
Set realistic heap sizes:
java -Xms128m -Xmx256m -jar quarkus-run.jar
Benefits:
-
Prevents JVM from consuming all container RAM
-
Avoids OOM kills in Kubernetes
-
Improves GC stability
7. Use G1 or Shenandoah GC (Java 17/21+)
G1 is the default and works well for most Quarkus apps, but Shenandoah offers:
-
Lower pause times
-
Better performance under tight memory constraints
Enable Shenandoah:
java -XX:+UseShenandoahGC -jar quarkus-run.jar
8. Minimize Thread Usage
Switch to Vert.x / Reactive extensions where possible.
Benefits:
-
Fewer long-lived threads
-
Lower per-thread memory overhead (256 KB stack per thread)
-
Better scalability
Example:
quarkus extension add quarkus-reactive-routes
9. Example: Memory Usage Before vs After Optimization
Example result on a typical Linux machine:
| Optimization Step | Memory Usage (RSS) |
|---|---|
| Baseline (default settings) | ~140 MB |
| Reduce JDBC pool + disable stats | ~120 MB |
| Unused bean removal + nested scan limits | ~100 MB |
| Caching + GC tuning | ~95 MB |
| Reactive SQL + reduced threads | ~80 MB |
Many real-world Quarkus apps comfortably run at 60–100 MB in JVM mode with proper configuration.
Your application now consumes significantly less memory in JVM mode and is ready for even more dramatic optimization using native builds.
Native Builds with GraalVM
One of Quarkus’s most powerful features is its ability to compile applications into native executables using GraalVM or Mandrel. Native builds offer:
-
Ultra-fast startup times (milliseconds)
-
Very low memory usage
-
Small container images
-
Faster autoscaling in Kubernetes
-
Ideal performance for serverless or microservices
In this section, you’ll install GraalVM (or Mandrel), build a native executable, measure performance gains, and fix common native build issues.
1. Understanding Native Builds
When you build a native image:
-
The JVM is not included
-
Code is ahead-of-time (AOT) compiled
-
Reflection, proxies, and dynamic class loading require configuration
-
Startup time improves drastically (often 10–100x faster)
-
Memory usage drops by up to 75%
Typical results:
| Mode | Startup Time | Memory (RSS) |
|---|---|---|
| JVM | ~0.7–1.0s | 120–150 MB |
| Native | 10–50 ms | 25–50 MB |
You’ll measure your actual numbers shortly.
2. Install GraalVM or Mandrel
Option A: Install GraalVM (JDK 21 recommended)
Using SDKMAN:
sdk install java 21-graal
sdk use java 21-graal
Verify:
java -version
Option B: Install Mandrel (Optimized for Quarkus)
Download Mandrel from the official releases or use SDKMAN:
sdk install java 21-mandrel
sdk use java 21-mandrel
3. Install Native Image Tool
With GraalVM:
gu install native-image
Check status:
native-image --version
4. Build a Native Executable
Inside your project:
./mvnw clean package -Pnative
Quarkus creates the native executable in:
target/quarkus-performance-1.0.0-runner
If you're using Gradle:
./gradlew build -Dquarkus.package.type=native
5. Run the Native Executable
Start the binary:
./target/quarkus-performance-1.0.0-runner
You should see startup logs like:
INFO ... started in 0.015s. Listening on: http://0.0.0.0:8080
That’s 15 milliseconds — over 50× faster than typical JVM mode.
6. Measure Memory Usage (Native)
Open a second terminal and run:
ps -o pid,rss,command | grep quarkus-performance
Example output:
51234 26000 ./quarkus-performance-1.0.0-runner
This means the app uses:
-
RSS = 26 MB
-
Nearly 5× less RAM than JVM mode
7. Test Throughput
Even though native mode excels at startup and memory usage, throughput is often comparable to JVM mode.
Using wrk:
wrk -t4 -c50 -d10s http://localhost:8080/hello
Or with hey:
hey -n 5000 -c 50 http://localhost:8080/hello
Depending on your CPU, throughput may be:
-
Slightly lower than JVM
-
Equal to JVM
-
Or significantly higher under high concurrency
Quarkus native performance varies based on workload type.
8. Common Native Build Issues & Fixes
Native builds require explicit configuration for:
-
Reflection
-
Dynamic proxies
-
Resources
-
SSL
-
Certain libraries (e.g., Jackson, Hibernate)
Below are common fixes.
8.1 Reflection Issues
If you see:
ClassNotFoundException: ... at runtime (reflection)
Add reflection config:
src/main/resources/reflection-config.json
[
{
"name": "com.djamware.entity.User",
"allDeclaredFields": true,
"allDeclaredConstructors": true,
"allDeclaredMethods": true
}
]
Tell Quarkus to include it:
quarkus.native.additional-build-args=--reflection-configuration-files=reflection-config.json
8.2 Missing Resources
If you load resources:
src/main/resources/myfile.txt
Add:
quarkus.native.resources.includes=*.txt
8.3 SSL Issues
Enable proper handling:
quarkus.native.enable-all-security-services=true
8.4 Jackson Serialization Issues
Enable full Jackson support:
quarkus.native.enable-https-url-handler=true
quarkus.jackson.fail-on-unknown-properties=false
9. Faster Native Builds with Docker (Recommended)
You can build native images without installing GraalVM locally.
Quarkus provides a containerized builder:
./mvnw package -Pnative -Dquarkus.native.container-build=true
Uses:
-
Mandrel or GraalVM
-
UBI container images
-
No local installation required
Good for CI/CD pipelines.
10. Native Build Result Summary
Typical results comparing JVM vs Native:
| Mode | Startup Time | Memory Usage | Image Size |
|---|---|---|---|
| JVM | ~0.7–1.0s | ~120–150 MB | ~80–120 MB |
| Native | ~10–50 ms | ~25–50 MB | ~15–30 MB |
Native builds shine in:
-
Serverless
-
Auto-scaling microservices
-
Low-memory environments
-
CLI tools/edge computing
Your application is now fully optimized for native mode performance.
Quarkus Build Optimization Tips
Quarkus delivers excellent performance out of the box, but you can push it even further with a few strategic build optimizations. These techniques reduce build time, minimize application size, shorten startup time, and improve overall runtime efficiency for both JVM and native builds.
This section covers the most effective build-time optimizations you should consider for production.
1. Enable Classpath Caching
Quarkus can cache classpath resolution across builds, which significantly improves:
-
Dev Mode startup (
quarkus dev) -
JVM build time
-
Native build time
Add this to application.properties:
quarkus.bootstrap.classpath-cache=true
Once enabled, Quarkus stores its cache in:
.quarkus/
You’ll see immediate improvements when restarting quarkus dev.
2. Use Fast JAR Packaging (Default)
Fast JAR packaging splits your app into:
-
quarkus-run.jar(thin bootstrap) -
quarkus-app/directory (lib classes/resources)
Ensure you’re using it:
quarkus.package.jar.type=fast-jar
Benefits:
-
Faster classpath scanning
-
Faster container startup
-
Smaller runtime footprint
3. Remove Dead Code at Build Time
Quarkus uses aggressive dead-code elimination through Arc.
Enable it:
quarkus.arc.remove-unused-beans=true
quarkus.arc.detect-unused-beans=all
4. Remove Unused Extensions (Large Impact)
To reduce build complexity:
List extensions:
quarkus info
Remove what you don’t need:
quarkus extension remove quarkus-smallrye-openapi
quarkus extension remove quarkus-security
quarkus extension remove quarkus-hibernate-validator
Every extension removed:
✔ shrinks application size
✔ shortens build time
✔ reduces startup time
5. Use Dependency Management Strategically
Audit your dependencies:
-
Remove unused
testlibraries -
Remove unused JDBC drivers
-
Replace heavier libs with lightweight ones
A leaner classpath = faster native builds.
6. Optimize Application Configuration for Build-Time Processing
Quarkus favors build-time configuration.
Move as many properties as possible to application.properties rather than environment variables, including:
-
HTTP port
-
JDBC URLs
-
Hibernate settings
-
Cache configs
The more Quarkus knows at build time, the fewer runtime optimizations it must do.
7. Use Container-Based Native Builds for Predictability
Native builds vary by OS. To ensure consistency:
./mvnw package -Pnative -Dquarkus.native.container-build=true
Benefits:
-
Deterministic builds
-
Works identically on any CI server
-
No need to install GraalVM locally
-
Faster when using container caching
8. Multi-Stage Docker Builds for Lightweight Images
The recommended Dockerfile:
FROM quay.io/quarkus/ubi-quarkus-native-image:latest AS build
COPY . /usr/src/app/
RUN mvn -f /usr/src/app/pom.xml -Pnative -Dquarkus.native.container-build=true package
FROM quay.io/quarkus/quarkus-micro-image:2.0
COPY --from=build /usr/src/app/target/*-runner /application
CMD ["./application"]
This produces:
-
Extremely small container images (~15–35 MB)
-
Fast cold starts
-
Great Kubernetes scaling behavior
9. Reduce Reflection With Proper Coding Style
Reflection slows down both JVM and native builds.
Avoid:
@Inject
MyService service;
Prefer:
MyService service;
public MyResource(MyService service) {
this.service = service;
}
Constructor injection results in:
-
Zero reflective lookups
-
Faster native builds (fewer config files)
-
Smaller binary
10. Use Build-Time Initialized Beans
Mark heavy components as build-time initialized if possible:
quarkus.native.additional-build-args=--initialize-at-build-time=com.djamware.service
This reduces:
-
Startup time
-
Static initializer cost
-
Warmup time in serverless environments
11. Reduce Log Output for Faster Startup
Logging slows startup during early initialization.
Tune it:
quarkus.log.level=INFO
quarkus.log.console.level=INFO
quarkus.log.category."io.quarkus".level=WARNING
Or for benchmarks:
quarkus.log.level=ERROR
12. Example: Impact of Build Optimizations
Here’s a typical before/after comparison:
| Build Stage | Before Optimization | After Optimization |
|---|---|---|
| JVM Build Time | ~9s | ~5s |
| Native Build Time | ~3m 10s | ~2m or less |
| Jar Size | ~30 MB | ~18 MB |
| Native Binary Size | ~55 MB | ~35 MB |
These improvements compound with the startup and memory optimizations previously covered.
Your Quarkus application is now leaner, builds faster, and produces smaller artifacts—all of which improve operational speed and efficiency.
Profiling & Monitoring Your App
Optimizing Quarkus performance doesn’t end with configuration tweaks—real improvements come from measuring and observing your application under real workloads. Profiling and monitoring help you:
-
Detect slow endpoints
-
Observe CPU and memory hotspots
-
Find memory leaks
-
Visualize thread usage
-
Track GC behavior
-
Measure request throughput and latency
-
Identify performance regressions early
This section covers the essential tools and approaches for profiling and monitoring both JVM and native Quarkus applications.
1. Use Micrometer + Prometheus Metrics
Since we enabled Micrometer earlier, Quarkus automatically exposes metrics at:
http://localhost:8080/q/metrics
This includes:
JVM Metrics
-
jvm_memory_used_bytes -
jvm_gc_pause_seconds -
process_cpu_usage
HTTP Metrics
-
http_server_requests_seconds_count -
http_server_requests_seconds_sum -
http_server_active_requests
Custom Metrics (Optional)
You can add your own:
@Inject
MeterRegistry registry;
@PostConstruct
void init() {
registry.counter("custom_user_hits").increment();
}
2. Set Up Prometheus for Scraping
Add Prometheus scrape config:
prometheus.yml
scrape_configs:
- job_name: 'quarkus'
static_configs:
- targets: ['host.docker.internal:8080']
Run:
prometheus --config.file=prometheus.yml
Now open Prometheus UI:
http://localhost:9090
Try queries like:
-
jvm_memory_used_bytes -
http_server_requests_seconds_count -
process_cpu_usage
3. Visualizing Metrics with Grafana (Optional)
Grafana dashboards are perfect for operational insights.
Import popular dashboards:
-
JVM Micrometer dashboard
-
Quarkus dashboard
-
Prometheus JVM stats
Sample charts include:
-
Heap usage over time
-
Request rate (RPS)
-
Database connection pool usage
-
Thread count
-
GC pause duration
4. Using JDK Flight Recorder (JFR)
JFR is the most powerful profiling tool for JVM mode.
Start your app with JFR enabled:
java -XX:StartFlightRecording=filename=recording.jfr -jar quarkus-run.jar
After running load tests, stop the recording:
jcmd <PID> JFR.stop name=1
Open the .jfr file with:
-
JDK Mission Control (JMC)
-
VisualVM with JFR plugin
What to analyze:
-
CPU hotspots
-
Memory allocations
-
GC behavior
-
Thread activity
-
Lock contentions
-
Exception rates
This helps identify slow services or high memory churn.
5. Profiling Native Images
Native binaries can also be profiled—using tools such as:
perf (Linux)
perf record -g ./target/app-runner
perf report
async-profiler
Start:
./profiler.sh -d 30 -f profile.html <PID>
Produces a flame graph (profile.html) showing:
-
Hot methods
-
Recursion points
-
Bottlenecks
-
Thread blocking
Valgrind / Massif
Memory-focused profiling:
valgrind --tool=massif ./app-runner
Outputs memory usage snapshots.
6. Using VisualVM (JVM Mode Only)
Start VisualVM:
jvisualvm
Attach to your Quarkus JVM process.
You can monitor:
-
Heap usage
-
GC activity
-
Thread details
-
CPU profiling
-
Memory allocation profiling
Memory leak detection:
-
Use Sampler → Memory
-
Watch for unbounded allocations
-
Identify retained objects
7. Enable Quarkus Built-In Trace Logging (For Troubleshooting)
quarkus.log.category."io.quarkus".level=TRACE
quarkus.log.level=INFO
Useful for diagnosing:
-
Startup delays
-
Bean initialization
-
Extension loading sequence
Remember to switch back to INFO in production.
8. Profiling Web Endpoints with Quarkus Dev UI
Run dev mode:
quarkus dev
Open:
http://localhost:8080/q/dev/
From here you can analyze:
-
HTTP request traces
-
Reactive routes activity
-
Build stats
-
Bean visualization
-
Configuration profiles
This is extremely useful for early debugging.
9. Load Testing to Produce Real Profiles
Combine tools:
wrk
wrk -t4 -c50 -d20s http://localhost:8080/hello
hey
hey -n 10000 -c 100 http://localhost:8080/hello
ab
ab -n 5000 -c 50 http://localhost:8080/hello
Run these while JFR or VisualVM is attached for realistic profiling under load.
10. What to Look For in Profiling Results
If CPU is high:
-
Hot methods → rewrite or optimize
-
Reflection → remove
-
Busy loops → fix
-
JSON serialization → use Jackson optimizations
If memory usage is high:
-
Too many threads → reduce
-
Large connection pools → shrink
-
Excessive object allocation → cache results
-
Memory leak → find retained objects
If throughput is low:
-
Blocking I/O → switch to reactive
-
Uneven GC → adjust heap sizes or GC type
-
Slow DB queries → add indexes or caching
Profiling and monitoring close the loop on performance optimization. With these tools, you can detect issues early, validate improvements, and ensure your Quarkus application stays highly efficient in production.
Real-World Example: Optimizing an API
To make all the concepts in this tutorial practical, we’ll walk through a real-world optimization scenario. We’ll build a simple User API using Quarkus + Hibernate Panache, measure its baseline performance, apply tuning techniques, and measure the performance improvements.
This section demonstrates how Quarkus performance tuning works in a real application—not just theoretical examples.
1. Create the User Entity
Create a simple Panache entity.
src/main/java/com/djamware/entity/User.java
package com.djamware.entity;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Entity;
@Entity
public class User extends PanacheEntity {
public String name;
public String email;
}
This gives us:
-
Auto-generated
id -
Basic CRUD features from Panache
2. Add Initial Sample Data
For testing performance, add sample users.
Update application.properties:
quarkus.hibernate-orm.sql-load-script=import.sql
Create src/main/resources/import.sql:
INSERT INTO user (id, name, email) VALUES (1, 'Alice', '[email protected]');
INSERT INTO user (id, name, email) VALUES (2, 'Bob', '[email protected]');
3. Create the UserResource REST API
src/main/java/com/djamware/resource/UserResource.java
package com.djamware.resource;
import java.util.List;
import com.djamware.entity.User;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@GET
public List<User> listUsers() {
return User.listAll();
}
@POST
@Transactional
public User create(User user) {
user.persist();
return user;
}
@GET
@Path("/{id}")
public User get(@PathParam("id") Long id) {
return User.findById(id);
}
}
Test in dev mode:
quarkus dev
Endpoints:
-
GET
/users -
POST
/users -
GET
/users/{id}
4. Baseline Performance Measurements
With JVM mode:
./mvnw package
java -jar target/quarkus-app/quarkus-run.jar
Measure:
Startup Time
Example:
started in 1.15s
Memory Usage
ps -o rss -p <PID>
Example baseline:
-
≈ 150 MB
Endpoint Throughput
Using wrk:
wrk -t4 -c50 -d20s http://localhost:8080/users
Example baseline:
-
~11k requests/sec
-
~3.5 ms avg latency
5. Apply Optimizations Step-by-Step
Now we apply the tuning techniques from earlier sections.
Step 1: Reduce DB Connection Pool
quarkus.datasource.jdbc.min-size=1
quarkus.datasource.jdbc.max-size=4
Step 2: Disable Unused ORM Features
quarkus.hibernate-orm.sql-load-script=import.sql
quarkus.hibernate-orm.second-level-caching-enabled=false
quarkus.hibernate-orm.statistics=false
Step 3: Enable Unused Bean Removal
quarkus.arc.remove-unused-beans=true
quarkus.arc.detect-unused-beans=all
Step 4: Enable Build-Time Optimization
quarkus.bootstrap.classpath-cache=true
Step 5: Migrate to Constructor Injection
Update UserResource (optional but recommended):
public UserResource() {
}
(Not much to inject for now, but useful in larger services.)
Step 6: Switch Logging Level
quarkus.log.level=INFO
quarkus.log.category."io.quarkus".level=WARNING
Step 7: Build a Native Executable (Optional)
./mvnw package -Pnative
Startup in native mode:
./target/quarkus-performance-1.0.0-runner
6. Performance After Optimization
JVM Mode Measurements
After applying optimizations:
| Metric | Baseline | After Optimizations |
|---|---|---|
| Startup Time | ~1.15s | ~0.72s |
| Memory (RSS) | ~150 MB | ~95 MB |
| Throughput (RPS) | ~11k | ~14k |
| Latency | ~3.5 ms | ~2.0 ms |
Major improvements:
-
~38% faster startup
-
~35% lower memory usage
-
~27% higher throughput
Native Mode Measurements
If using native builds:
| Metric | JVM (optimized) | Native |
|---|---|---|
| Startup Time | ~0.72s | 0.018s |
| Memory Usage | ~95 MB | ~32 MB |
| Throughput | ~14k | ~12.5k–14k |
Native builds:
-
Drastically reduce startup time
-
Reduce memory by ~70%
-
Offer similar throughput under load
7. Summary of Real-World Optimization
This practical example demonstrates how small configuration changes—connection pool size, ORM tuning, unused bean removal, logging tweaks, and classpath caching—can produce big gains in performance.
Key Takeaways:
-
Database connection pools often waste memory unless sized properly
-
Hibernate’s statistics and caching can be disabled for simple CRUD APIs
-
Bean removal significantly shrinks the runtime footprint
-
Classpath caching accelerates the dev experience and builds
-
Native mode offers ultra-fast cold starts and tiny memory usage
Your real-world Quarkus API is now significantly faster, lighter, and more efficient—validated by actual benchmarks rather than assumptions.
Deployment Best Practices
Once your Quarkus application is optimized for startup time, memory usage, and native execution, the next step is deploying it efficiently. Quarkus is designed for cloud-native environments and works especially well in containers and Kubernetes. This section covers best practices for packaging, resource sizing, scaling, and production configurations.
1. Use Multi-Stage Docker Builds
To produce the smallest and fastest containers, always build using Quarkus’s recommended multi-stage Docker approach.
Dockerfile (JVM Mode)
FROM maven:3.9.6-eclipse-temurin-21 AS build
WORKDIR /usr/src/app
COPY . .
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /usr/src/app/target/quarkus-app/ ./
CMD ["java", "-jar", "quarkus-run.jar"]
Dockerfile (Native Mode)
FROM quay.io/quarkus/ubi-quarkus-native-image:latest AS build
COPY . /usr/src/app/
RUN mvn -f /usr/src/app/pom.xml -Pnative -Dquarkus.native.container-build=true package
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
CMD ["./application"]
Benefits:
-
Very small image size (20–40 MB)
-
Fast startup time in environments like Kubernetes or serverless
-
Portable and consistent builds
2. Use Distroless Images in Production (Optional)
Distroless images remove unnecessary tools from the container, reducing the attack surface.
Example base image:
FROM gcr.io/distroless/base
Use only for final runtime images—not for builds.
3. Configure Memory Limits Explicitly
When running in Docker or Kubernetes, always define memory limits.
Example Docker run:
docker run -m 256m --memory-swap 256m quarkus-app
Example Kubernetes manifest:
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
Why this matters:
-
JVM respects container limits
-
Prevents OOM kills
-
Ensures predictable performance
4. Optimize JVM Settings for Containers
Add JVM flags (for JVM mode apps):
java -XX:+UseContainerSupport \
-Xms64m \
-Xmx128m \
-XX:MaxMetaspaceSize=128m \
-jar quarkus-run.jar
Or via JAVA_OPTS:
export JAVA_OPTS="-Xms64m -Xmx128m"
Recommended for Quarkus JVM builds:
-
Low initial heap sizes
-
G1GC or Shenandoah GC
-
Keep metaspace small
5. Enable Liveness & Readiness Probes
Quarkus integrates with Kubernetes health endpoints out of the box.
Add the health extension:
quarkus extension add quarkus-smallrye-health
Endpoints:
-
Liveness:
/q/health/live -
Readiness:
/q/health/ready
Kubernetes example:
livenessProbe:
httpGet:
path: /q/health/live
port: 8080
readinessProbe:
httpGet:
path: /q/health/ready
port: 8080
Probes ensure:
-
Pods only receive traffic when ready
-
Faulty pods are restarted automatically
6. Enable Metrics Scraping with Prometheus
If Prometheus is used, expose metrics:
quarkus.micrometer.export.prometheus.enabled=true
Kubernetes ServiceMonitor example:
endpoints:
- port: http
path: /q/metrics
This enables autoscaling based on:
-
Request rate
-
CPU load
-
Memory usage
-
Custom business metrics
7. Autoscaling Quarkus in Kubernetes (HPA + KEDA)
Horizontal Pod Autoscaler (CPU-based)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: quarkus-api
spec:
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
KEDA Scalers (Event-Driven Autoscaling)
KEDA supports:
-
Kafka
-
RabbitMQ
-
Redis
-
Postgres triggers
-
Prometheus queries
Perfect for event-driven microservices where the load fluctuates.
8. Use Native Mode When Scaling Matters
Native binaries excel in:
-
Serverless functions
-
Autoscaling microservices
-
Low-memory environments
-
High-density Kubernetes clusters
-
Edge or IoT devices
Benefits:
-
Millisecond startup time
-
~70% less memory usage
-
High pod density per node
-
Near-zero cold starts
9. Use ConfigMaps and Secrets Correctly
Store environment-specific config externally:
Kubernetes ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: quarkus-config
data:
APPLICATION_ENV: production
Secret
apiVersion: v1
kind: Secret
metadata:
name: db-secret
stringData:
username: admin
password: secret
Use them in Quarkus via:
quarkus.datasource.username=${DB_USERNAME}
quarkus.datasource.password=${DB_PASSWORD}
10. Log Aggregation for Production
Use structured JSON logs:
quarkus.log.console.json=true
Tools:
-
Elastic Stack (ELK)
-
Loki + Promtail
-
Datadog
-
Splunk
This supports:
-
Better filtering
-
Distributed tracing
-
Log correlation
11. Secure Your Quarkus Deployment
Best practices:
-
Disable unused endpoints in production
-
Enforce HTTPS (reverse proxy or Ingress)
-
Use OAuth2, OIDC, or JWT for authentication
-
Disable Dev UI in production:
quarkus.dev-ui.enabled=false
-
Ensure RBAC on Kubernetes
-
Keep container images minimal
12. Example: Production-Ready Resource Settings
Sample full Kubernetes Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: quarkus-api
spec:
replicas: 2
selector:
matchLabels:
app: quarkus-api
template:
metadata:
labels:
app: quarkus-api
spec:
containers:
- name: quarkus-api
image: djamware/quarkus-api:1.0.0
resources:
requests:
cpu: "150m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: quarkus-config
- secretRef:
name: db-secret
livenessProbe:
httpGet:
path: /q/health/live
port: 8080
readinessProbe:
httpGet:
path: /q/health/ready
port: 8080
13. Summary
A well-deployed Quarkus application should:
-
Use optimized container images
-
Respect resource limits
-
Provide health checks and metrics
-
Support autoscaling
-
Store config securely
-
Run with minimal overhead
Following these deployment practices ensures you get the maximum performance, scalability, and reliability from your Quarkus application in production.
Conclusion
Quarkus is designed with performance at its core, but the real power comes when you combine its built-in optimizations with the tuning techniques we explored in this tutorial. By understanding how Quarkus handles build-time processing, runtime initialization, resource management, and native compilation, you can build applications that are incredibly fast, lightweight, and scalable.
In this guide, you learned how to:
-
Set up a baseline Quarkus project
-
Measure startup time, memory usage, and throughput
-
Reduce startup time through smarter configuration, dependency trimming, and build-time optimizations
-
Reduce memory consumption by tuning database pools, disabling unused ORM features, and optimizing CDI
-
Build ultra-fast native images with GraalVM or Mandrel
-
Profile and monitor your application using JFR, VisualVM, Prometheus, and Micrometer
-
Apply optimizations to a real-world User API and see measurable improvements
-
Deploy Quarkus applications efficiently using Docker, Kubernetes, resource limits, autoscaling, and native builds
With these practices, you can confidently build Quarkus applications that start in milliseconds, run on minimal memory, and scale instantly — whether you’re deploying to Kubernetes clusters, serverless platforms, or edge devices.
Performance optimization is an ongoing process, so keep profiling, testing, and iterating. As your application grows, the techniques in this tutorial will help you maintain exceptional efficiency and reliability in production.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about Quarkus and Microservices, you can take the following cheap course:
- Cloud-native Microservices with Quarkus
- Building Microservices with Quarkus
- (2025) Quarkus for beginners, everything you need to know.
- Accessing Relational Databases with Quarkus
- Learn Vert.x - Reactive microservices with Java
- Quarkus - Simple REST API and Unit Tests with JUnit 5
- The complete guide to running Java in Docker and Kubernetes
- The Complete Microservices With Java
Thanks!
