Inside Quarkus: Real-Time Observability with Java Flight Recorder
Discover how Quarkus now records runtime and build metadata directly into JFR events — perfect for profiling, debugging, and version tracking.
When your Quarkus app starts up, it does a lot in just a few hundred milliseconds. Configuration, dependency injection, build-time metadata, HTTP initialization. Everything happens fast. Too fast to watch.
What if you could peek behind the curtain and see exactly what your Quarkus app is doing at runtime, which Java version it’s using, which Quarkus version it was built with, even how your endpoints behave under load?
That’s where Java Flight Recorder (JFR) comes in.
With Quarkus 3.26 and the updated quarkus-jfr extension, JFR now captures rich runtime metadata automatically. You can finally answer questions like:
Which version of the app is actually running in production?
How long does each REST call take?
When was the app built and on what JDK?
Let’s explore this with a simple, runnable project.
What You’ll Build
You’ll create a small Product Catalog Service that uses JFR to record runtime data. You’ll:
Set up Quarkus with the JFR extension
Add a few REST endpoints
Capture and analyze runtime events
Visualize them with JDK Mission Control
Estimated time: 30–45 minutes
Requirements:
JDK 21+
Maven 3.9+
Optional: JDK Mission Control (JMC)
Creating the Project
Let’s start fresh. (Or, if you just want to look at the code, grab it from my Github repository!)
mvn io.quarkus:quarkus-maven-plugin:create \
-DprojectGroupId=com.example \
-DprojectArtifactId=jfr-runtime-demo \
-Dextensions="rest-jackson,jfr"
cd jfr-runtime-demoAlternatively, if you use the Quarkus CLI:
quarkus create app com.example:jfr-runtime-demo --extension=rest-jackson,jfrBuilding a Small REST API
Rename the GreetingResource to src/main/java/com/example/ProductResource.java and replace the content with:
package com.example;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
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(”/products”)
@Produces(MediaType.APPLICATION_JSON)
public class ProductResource {
private static final Random random = new Random();
private static final List<Product> products = new ArrayList<>();
static {
products.add(new Product(1L, “Laptop”, 999.99));
products.add(new Product(2L, “Mouse”, 29.99));
products.add(new Product(3L, “Keyboard”, 79.99));
}
@GET
public List<Product> listAll() {
simulateWork(50, 150);
return products;
}
@GET
@Path(”/{id}”)
public Product getById(@PathParam(”id”) Long id) {
simulateWork(20, 80);
return products.stream()
.filter(p -> p.getId().equals(id))
.findFirst()
.orElseThrow(() -> new NotFoundException(”Product not found”));
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Product create(Product product) {
simulateWork(100, 300);
product.setId((long) (products.size() + 1));
products.add(product);
return product;
}
private void simulateWork(int min, int max) {
try {
Thread.sleep(random.nextInt(max - min) + min);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public static class Product {
public Long id;
public String name;
public Double price;
public Product() {
}
public Product(Long id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}
}
Add metadata in application.properties:
quarkus.application.name=Product Catalog Service
quarkus.application.version=1.0.0
quarkus.http.port=8080
quarkus.jfr.enabled=trueDon’t forget to delete the tests in src/main/test/ 🤪
Start dev mode:
mvn quarkus:devOpen http://localhost:8080/products. You’ll get your mock catalog back in JSON.
Recording a Flight
Let’s capture the app in action.
Option A: Start recording immediately
quarkus dev -Djvm.args="-XX:StartFlightRecording=name=quarkus,dumponexit=true,filename=quarkus-recording.jfr"Option B: Start manually with jcmd
If you prefer to attach later:
# Start the app normally
mvn quarkus:dev
# In another terminal
jcmd | grep quarkus
jcmd <PID> JFR.start name=quarkus-demo settings=profile filename=quarkus-recording.jfrGenerate some traffic:
for i in {1..5}; do
curl http://localhost:8080/products
curl http://localhost:8080/products/1
done
curl -X POST http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"name":"Monitor","price":299.99}'Stop the recording:
jcmd <PID> JFR.stop name=quarkus-demoA file quarkus-recording.jfr should appear in your directory.
Inspecting the Data
The simplest way to inspect your capture:
jfr summary quarkus-recording.jfrTo print everything:
jfr print quarkus-recording.jfr > all-events.txtFilter only Quarkus events:
jfr print --categories Quarkus quarkus-recording.jfrYou’ll see entries like:
quarkus.RuntimeInfo {
startTime = 2025-10-26T09:42:33.000Z
quarkusVersion = “3.26.0”
javaVersion = “17.0.11”
javaVendor = “Eclipse Adoptium”
applicationName = “Product Catalog Service”
applicationVersion = “1.0.0”
buildTime = “2025-10-26T09:40:18.000Z”
}That’s build-time and runtime metadata captured automatically. No annotations, no code.
Visualizing in JDK Mission Control
If you like pictures over text, download, install and open JMC.
jmcThen:
File → Open File → select
quarkus-recording.jfrExpand Event Browser → Quarkus
Explore:
quarkus.RuntimeInfo – startup and version metadata
quarkus.RestStart / RestEnd / Rest – HTTP timing events
Sort by duration, identify slow endpoints, and correlate with GC or allocation events.
JFR gives you an honest look at how your app behaves under real workloads.
Adding a Custom Business Event
You can also add your own JFR events.
Create src/main/java/com/example/BusinessEvent.java:
package com.example;
import jdk.jfr.Category;
import jdk.jfr.Event;
import jdk.jfr.Label;
import jdk.jfr.Name;
@Name(”com.example.BusinessOperation”)
@Label(”Business Operation”)
@Category(”Application”)
public class BusinessEvent extends Event {
@Label(”Operation”)
String operation;
@Label(”User ID”)
String userId;
@Label(”Items Processed”)
int items;
public BusinessEvent(String op, String user, int items) {
this.operation = op;
this.userId = user;
this.items = items;
}
public static void record(String op, String user, int items) {
BusinessEvent e = new BusinessEvent(op, user, items);
e.commit();
}
}
Use it inside your resource:
import jakarta.ws.rs.core.Response;
@POST
@Path(”/batch”)
public Response processBatch(List<Product> batch) {
batch.forEach(this::create);
BusinessEvent.record(”BATCH_IMPORT”, “user123”, batch.size());
return Response.ok().build();
}Run again, send a batch request.
curl -X POST http://localhost:8080/products/batch -H “Content-Type: application/json” -d '[{"name": "Gaming Mouse", "price": 49.99}, {"name": "Mechanical Keyboard", "price": 129.99}, {"name": "Monitor Stand", "price": 39.99}]'And open the new .jfr file.
You’ll now see your custom com.example.BusinessOperation events alongside Quarkus ones.
jfr print --categories Application quarkus-recording.jfr
com.example.BusinessOperation {
startTime = 05:50:46.444 (2025-10-26)
operation = “BATCH_IMPORT”
userId = “user123”
items = 3
eventThread = “executor-thread-1” (javaThreadId = 107)
stackTrace = [
com.example.BusinessEvent.record(String, String, int) line: 27
com.example.ProductResource.processBatch(List) line: 60
com.example.ProductResource$quarkusrestinvoker$processBatch_9684313dc7f48dc363f00285afce99149378dd7e.invoke(Object, Object[])
org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(ResteasyReactiveRequestContext) line: 29
io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(int) line: 183
...
]
}Troubleshooting and Native Notes
No Quarkus events showing up?
Make sure quarkus-jfr is loaded and events are enabled:
jfr print --events quarkus.RuntimeInfo quarkus-recording.jfrRecording not created?
Check write permissions or specify a full path:
-XX:StartFlightRecording=filename=/tmp/recording.jfrBuilding a native image?
Enable JFR in native mode:
mvn clean package -Dnative -Dquarkus.native.monitoring=jfrEven in native executables, JFR works with minimal overhead.
Why This Matters
This small feature tells a bigger story about Quarkus:
Observability by default. You get structured, production-grade data without extra code.
Near-zero overhead. JFR typically costs less than 1 % runtime overhead.
Interoperable. Quarkus JFR data lines up neatly with OpenTelemetry traces.
Consistent. It works the same in JVM and native modes.
You can finally see what your Quarkus app knows about itself.
Try This Next
Feeling adventurous?
Add Hibernate Panache and capture DB calls
Record JFR data under heavy load
Compare JVM vs native startup profiles
Create dashboards or automate summaries with the
jfrCLI
Once you do, you’ll realize Quarkus doesn’t just start fast — it tells its story fast too.
Key takeaway:
Quarkus now records its own heartbeat. And thanks to Java Flight Recorder, you can listen in anytime.



