Sitemap
Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Optimizing Quarkus Performance: Startup Time, Memory & Native Builds

17 min readNov 20, 2025

--

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:

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.

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?

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

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:

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:

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:

Alternatively, you can watch usage over time:

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:

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:

These metrics help validate the accuracy of your manual measurements.

5. Summary: Baseline Results Table

Create a table to capture your baseline. Example:

Your exact numbers may differ, but the goal is to establish your own baseline.

We will revisit these metrics after:

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:

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:

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:

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:

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:

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:

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:

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:

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:

7. Use G1 or Shenandoah GC (Java 17/21+)

G1 is the default and works well for most Quarkus apps, but Shenandoah offers:

Enable Shenandoah:

java -XX:+UseShenandoahGC -jar quarkus-run.jar

8. Minimize Thread Usage

Switch to Vert.x / Reactive extensions where possible.

Benefits:

Example:

quarkus extension add quarkus-reactive-routes

9. Example: Memory Usage Before vs After Optimization

Example result on a typical Linux machine:

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:

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:

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

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:

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:

Quarkus native performance varies based on workload type.

8. Common Native Build Issues & Fixes

Native builds require explicit configuration for:

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:

Good for CI/CD pipelines.

10. Native Build Result Summary

Typical results comparing JVM vs Native:

Native builds shine in:

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:

Get Didin Jamaludin’s stories in your inbox

Join Medium for free to get updates from this writer.

Add this to application.properties:

quarkus.bootstrap.classpath-cache=true

Once enabled, Quarkus stores its cache in:

You’ll see immediate improvements when restarting quarkus dev.

2. Use Fast JAR Packaging (Default)

Fast JAR packaging splits your app into:

Ensure you’re using it:

quarkus.package.jar.type=fast-jar

Benefits:

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:

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:

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:

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:

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:

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:

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

HTTP Metrics

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:

3. Visualizing Metrics with Grafana (Optional)

Grafana dashboards are perfect for operational insights.

Import popular dashboards:

Sample charts include:

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:

What to analyze:

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:

Valgrind / Massif

Memory-focused profiling:

valgrind --tool=massif ./app-runner

Outputs memory usage snapshots.

6. Using VisualVM (JVM Mode Only)

Start VisualVM:

Attach to your Quarkus JVM process.

You can monitor:

Memory leak detection:

7. Enable Quarkus Built-In Trace Logging (For Troubleshooting)

quarkus.log.category."io.quarkus".level=TRACE quarkus.log.level=INFO

Useful for diagnosing:

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:

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:

If memory usage is high:

If throughput is low:

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:

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:

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

Example baseline:

Endpoint Throughput

Using wrk:

wrk -t4 -c50 -d20s http://localhost:8080/users

Example baseline:

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:

Major improvements:

Native Mode Measurements

If using native builds:

Native builds:

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:

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:

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:

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:

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:

Kubernetes example:

livenessProbe: httpGet: path: /q/health/live port: 8080 readinessProbe: httpGet: path: /q/health/ready port: 8080

Probes ensure:

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:

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:

Perfect for event-driven microservices where the load fluctuates.

8. Use Native Mode When Scaling Matters

Native binaries excel in:

Benefits:

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:

This supports:

11. Secure Your Quarkus Deployment

Best practices:

quarkus.dev-ui.enabled=false

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.

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!

Originally published at https://www.djamware.com.

--

Stackademic
Stackademic

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.