Optimizing Quarkus Performance: Startup Time, Memory & Native Builds

by Didin J. on Nov 20, 2025 Optimizing Quarkus Performance: Startup Time, Memory & Native Builds

Optimize Quarkus performance with faster startup times, lower memory usage, native builds, and deployment best practices for high-efficiency cloud-native apps.

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 test libraries

  • 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:

Thanks!